use engine_input_producers::EngineInputV2;
use engine_style_parser::{ParserBoundarySyntaxFactsV0, StyleSemanticFactsV0};
use serde::Serialize;
use crate::summarize_source_input_evidence;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SemanticPromotionEvidenceSummaryV0 {
pub schema_version: &'static str,
pub product: &'static str,
pub items: Vec<SemanticPromotionEvidenceItemV0>,
pub blocking_gaps: Vec<&'static str>,
pub next_priorities: Vec<&'static str>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SemanticPromotionEvidenceItemV0 {
pub evidence: &'static str,
pub status: &'static str,
pub provider: &'static str,
pub observed_count: usize,
pub reason: String,
}
pub fn summarize_semantic_promotion_evidence(
parser_facts: &ParserBoundarySyntaxFactsV0,
semantic_facts: &StyleSemanticFactsV0,
) -> SemanticPromotionEvidenceSummaryV0 {
let source_span_ready = parser_facts.lossless_cst.all_token_spans_within_source
&& parser_facts.lossless_cst.all_node_spans_within_source;
let unresolved_sass_count = semantic_facts
.sass
.selectors_with_unresolved_variable_refs_names
.len()
+ semantic_facts
.sass
.selectors_with_unresolved_mixin_includes_names
.len();
let rewrite_blocker_count =
semantic_facts.selector_identity.nested_unsafe_names.len() + unresolved_sass_count;
let custom_property_seed_count = parser_facts.custom_properties.decl_names.len()
+ parser_facts.custom_properties.ref_names.len();
let items = vec![
SemanticPromotionEvidenceItemV0 {
evidence: "selectorCanonicalId",
status: if semantic_facts.selector_identity.canonical_names.is_empty() {
"gap"
} else {
"ready"
},
provider: "StyleSelectorIdentityFactsV0.canonicalNames",
observed_count: semantic_facts.selector_identity.canonical_names.len(),
reason: format!(
"{} canonical selector ids are exposed",
semantic_facts.selector_identity.canonical_names.len()
),
},
SemanticPromotionEvidenceItemV0 {
evidence: "sourceSpan",
status: if source_span_ready { "ready" } else { "gap" },
provider: "ParserLosslessCstFactsV0",
observed_count: parser_facts.lossless_cst.token_count,
reason: format!(
"token spans valid={} node spans valid={}",
parser_facts.lossless_cst.all_token_spans_within_source,
parser_facts.lossless_cst.all_node_spans_within_source
),
},
SemanticPromotionEvidenceItemV0 {
evidence: "bindingOrigin",
status: "partial",
provider: "StyleSassSemanticFactsV0.selectorSymbolFacts",
observed_count: semantic_facts.sass.selector_symbol_facts.len(),
reason: format!(
"{} Sass selector symbol facts are exposed; cross-file origin still needs source bridge evidence",
semantic_facts.sass.selector_symbol_facts.len()
),
},
SemanticPromotionEvidenceItemV0 {
evidence: "styleModuleEdge",
status: if parser_facts.sass.module_use_edges.is_empty() {
"partial"
} else {
"ready"
},
provider: "ParserSassSyntaxFactsV0.moduleUseEdges",
observed_count: parser_facts.sass.module_use_edges.len(),
reason: format!(
"{} Sass module use edges are exposed",
parser_facts.sass.module_use_edges.len()
),
},
SemanticPromotionEvidenceItemV0 {
evidence: "valueDomainExplanation",
status: "partial",
provider: "ParserIndexValueFactsV0",
observed_count: parser_facts.values.ref_names.len(),
reason: format!(
"{} value refs are exposed; source-side expression-domain explanation remains external",
parser_facts.values.ref_names.len()
),
},
SemanticPromotionEvidenceItemV0 {
evidence: "designTokenSeed",
status: if custom_property_seed_count == 0 {
"partial"
} else {
"ready"
},
provider: "ParserIndexCustomPropertyFactsV0",
observed_count: custom_property_seed_count,
reason: format!(
"{} CSS custom property declarations and {} var() references are exposed",
parser_facts.custom_properties.decl_names.len(),
parser_facts.custom_properties.ref_names.len()
),
},
SemanticPromotionEvidenceItemV0 {
evidence: "rewriteSafetyBlocker",
status: if rewrite_blocker_count == 0 {
"ready"
} else {
"partial"
},
provider: "StyleSelectorIdentityFactsV0 + StyleSassSemanticFactsV0",
observed_count: rewrite_blocker_count,
reason: format!("{rewrite_blocker_count} selector or Sass blockers are exposed"),
},
SemanticPromotionEvidenceItemV0 {
evidence: "referenceSiteIdentity",
status: "gap",
provider: "EngineInputV2.selector-usage",
observed_count: 0,
reason: "reference sites are produced outside this parser-backed semantic boundary"
.to_string(),
},
SemanticPromotionEvidenceItemV0 {
evidence: "certaintyReason",
status: "gap",
provider: "EngineInputV2.type-facts",
observed_count: 0,
reason: "source-side certainty reasons are not yet carried into omena-semantic"
.to_string(),
},
];
SemanticPromotionEvidenceSummaryV0 {
schema_version: "0",
product: "omena-semantic.promotion-evidence",
items,
blocking_gaps: vec!["referenceSiteIdentity", "certaintyReason"],
next_priorities: vec!["referenceSiteIdentity", "certaintyReason", "bindingOrigin"],
}
}
pub fn summarize_semantic_promotion_evidence_with_source_input(
parser_facts: &ParserBoundarySyntaxFactsV0,
semantic_facts: &StyleSemanticFactsV0,
input: &EngineInputV2,
) -> SemanticPromotionEvidenceSummaryV0 {
let source_evidence = summarize_source_input_evidence(input);
let mut summary = summarize_semantic_promotion_evidence(parser_facts, semantic_facts);
for item in &mut summary.items {
match item.evidence {
"bindingOrigin" => {
item.status = source_evidence.binding_origin.status;
item.provider = "EngineInputV2.class-expressions";
item.observed_count = source_evidence.binding_origin.expression_count;
item.reason = format!(
"{} source class expressions expose binding origins",
source_evidence.binding_origin.expression_count
);
}
"styleModuleEdge" => {
item.status = source_evidence.style_module_edge.status;
item.provider = "EngineInputV2.source-style-edges";
item.observed_count = source_evidence.style_module_edge.source_style_edge_count;
item.reason = format!(
"{} source-to-style module edges are linked",
source_evidence.style_module_edge.source_style_edge_count
);
}
"valueDomainExplanation" => {
item.status = source_evidence.value_domain_explanation.status;
item.provider = "EngineInputV2.expression-semantics";
item.observed_count = source_evidence.value_domain_explanation.expression_count;
item.reason = format!(
"{} source expressions expose value-domain explanations",
source_evidence.value_domain_explanation.expression_count
);
}
"referenceSiteIdentity" => {
item.status = source_evidence.reference_site_identity.status;
item.provider = "EngineInputV2.selector-usage";
item.observed_count = source_evidence.reference_site_identity.reference_site_count;
item.reason = format!(
"{} selector reference sites are identity-preserving",
source_evidence.reference_site_identity.reference_site_count
);
}
"certaintyReason" => {
item.status = source_evidence.certainty_reason.status;
item.provider = "EngineInputV2.expression-semantics";
item.observed_count = source_evidence.certainty_reason.expression_count;
item.reason = format!(
"{} source expressions expose selector certainty reasons",
source_evidence.certainty_reason.expression_count
);
}
_ => {}
}
}
summary.blocking_gaps = summary
.items
.iter()
.filter(|item| item.status == "gap")
.map(|item| item.evidence)
.collect();
summary.next_priorities = source_evidence.blocking_gaps;
summary
}