Skip to main content

omena_semantic/
selector_identity.rs

1//! Canonical selector identity summaries.
2//!
3//! This module exposes the semantic identity layer that normalizes selector
4//! aliases, tracks rewrite safety, and provides stable counts for parser-to-LSP
5//! selector equivalence gates.
6
7use std::collections::BTreeSet;
8
9use serde::Serialize;
10
11use crate::StyleSelectorIdentityFactsV0;
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
14#[serde(rename_all = "camelCase")]
15pub struct SelectorIdentityEngineSummaryV0 {
16    pub schema_version: &'static str,
17    pub product: &'static str,
18    pub canonical_id_count: usize,
19    pub canonical_ids: Vec<SelectorCanonicalIdentityV0>,
20    pub rewrite_safety: SelectorIdentityRewriteSafetyV0,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
24#[serde(rename_all = "camelCase")]
25pub struct SelectorCanonicalIdentityV0 {
26    pub canonical_id: String,
27    pub local_name: String,
28    pub identity_kind: &'static str,
29    pub rewrite_safety: &'static str,
30    pub blockers: Vec<&'static str>,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
34#[serde(rename_all = "camelCase")]
35pub struct SelectorIdentityRewriteSafetyV0 {
36    pub all_canonical_ids_rewrite_safe: bool,
37    pub safe_canonical_ids: Vec<String>,
38    pub blocked_canonical_ids: Vec<String>,
39    pub blockers: Vec<&'static str>,
40}
41
42pub fn summarize_selector_identity_engine(
43    facts: &StyleSelectorIdentityFactsV0,
44) -> SelectorIdentityEngineSummaryV0 {
45    let bem_safe = facts
46        .bem_suffix_safe_names
47        .iter()
48        .cloned()
49        .collect::<BTreeSet<_>>();
50    let nested_unsafe = facts
51        .nested_unsafe_names
52        .iter()
53        .cloned()
54        .collect::<BTreeSet<_>>();
55
56    let canonical_ids = facts
57        .canonical_names
58        .iter()
59        .map(|name| {
60            let blockers = if nested_unsafe.contains(name) {
61                vec!["nested-expansion"]
62            } else {
63                Vec::new()
64            };
65            SelectorCanonicalIdentityV0 {
66                canonical_id: format!("selector:{name}"),
67                local_name: name.clone(),
68                identity_kind: if bem_safe.contains(name) {
69                    "bemSuffix"
70                } else {
71                    "localClass"
72                },
73                rewrite_safety: if blockers.is_empty() {
74                    "safe"
75                } else {
76                    "blocked"
77                },
78                blockers,
79            }
80        })
81        .collect::<Vec<_>>();
82
83    let safe_canonical_ids = canonical_ids
84        .iter()
85        .filter(|identity| identity.blockers.is_empty())
86        .map(|identity| identity.canonical_id.clone())
87        .collect::<Vec<_>>();
88    let blocked_canonical_ids = canonical_ids
89        .iter()
90        .filter(|identity| !identity.blockers.is_empty())
91        .map(|identity| identity.canonical_id.clone())
92        .collect::<Vec<_>>();
93
94    SelectorIdentityEngineSummaryV0 {
95        schema_version: "0",
96        product: "omena-semantic.selector-identity",
97        canonical_id_count: canonical_ids.len(),
98        canonical_ids,
99        rewrite_safety: SelectorIdentityRewriteSafetyV0 {
100            all_canonical_ids_rewrite_safe: blocked_canonical_ids.is_empty(),
101            safe_canonical_ids,
102            blocked_canonical_ids,
103            blockers: if nested_unsafe.is_empty() {
104                Vec::new()
105            } else {
106                vec!["nested-expansion"]
107            },
108        },
109    }
110}