Skip to main content

omena_lsp_server/
frame_aware_refresh.rs

1use std::collections::BTreeMap;
2
3use omena_cascade::{
4    DiagnosticFrameFootprintV0, RecheckSelectionV0, compute_edit_footprint, select_recheck_set,
5};
6use omena_incremental::{
7    IncrementalGraphInputV0, IncrementalNodeInputV0, IncrementalRevisionV0,
8    OmenaIncrementalDatabaseV0,
9};
10use omena_query::{
11    OmenaParserStyleDialect, OmenaQueryStyleFrameRefreshFactsV0,
12    OmenaQueryStyleFrameRefreshParseCacheV0,
13    summarize_omena_query_style_frame_refresh_facts_with_reuse,
14};
15use serde::Serialize;
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
18#[serde(rename_all = "camelCase")]
19pub struct FrameAwareRefreshReportV0 {
20    pub schema_version: &'static str,
21    pub product: &'static str,
22    pub feature_gate: &'static str,
23    pub selective_refresh_enabled: bool,
24    pub edited_module_count: usize,
25    pub selected_diagnostic_instance_ids: Vec<String>,
26    pub skipped_diagnostic_instance_ids: Vec<String>,
27    pub layer_marker: &'static str,
28}
29
30#[derive(Debug, Clone, PartialEq, Serialize)]
31#[serde(rename_all = "camelCase")]
32pub struct FrameAwareRefreshComparisonV0 {
33    pub schema_version: &'static str,
34    pub product: &'static str,
35    pub fixed_workspace_fixture: &'static str,
36    pub diagnostic_frame_count: usize,
37    pub edited_module_count: usize,
38    pub unconditional_selected_count: usize,
39    pub selective_selected_count: usize,
40    pub skipped_diagnostic_count: usize,
41    pub selected_work_reduction_count: usize,
42    pub selected_work_reduction_ratio: f64,
43    pub measured_latency_proxy: &'static str,
44    pub selective_refresh_enabled_by_default: bool,
45    pub feature_gate: &'static str,
46    pub disable_gate: &'static str,
47    pub layer_marker: &'static str,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct FrameAwareStyleModuleInputV0 {
52    pub module_id: String,
53    pub source: String,
54    pub dialect: OmenaParserStyleDialect,
55}
56
57#[derive(Default)]
58pub struct FrameAwareRefreshRuntimeV0 {
59    revision: u64,
60    incremental_database: OmenaIncrementalDatabaseV0,
61    parse_caches_by_module_id: BTreeMap<String, OmenaQueryStyleFrameRefreshParseCacheV0>,
62}
63
64impl FrameAwareRefreshRuntimeV0 {
65    pub fn refresh_diagnostics_with_style_modules_policy(
66        &mut self,
67        frames: &[DiagnosticFrameFootprintV0],
68        modules: &[FrameAwareStyleModuleInputV0],
69        selective_refresh_enabled: bool,
70    ) -> FrameAwareRefreshReportV0 {
71        let graph = self.frame_refresh_graph_input(modules);
72        let update = self
73            .incremental_database
74            .plan_and_upsert_graph_input(&graph);
75        let dirty_module_ids = update
76            .incremental_plan
77            .nodes
78            .iter()
79            .filter(|node| node.dirty)
80            .map(|node| node.id.clone())
81            .collect::<Vec<_>>();
82        let edited_module_count = dirty_module_ids.len();
83        let selection = if selective_refresh_enabled {
84            select_recheck_set_for_module_ids(frames, dirty_module_ids)
85        } else {
86            select_recheck_set_for_module_ids(frames, frames_to_module_ids(frames))
87        };
88
89        FrameAwareRefreshReportV0 {
90            schema_version: "0",
91            product: "omena-lsp-server.frame-aware-refresh",
92            feature_gate: "OMENA_LSP_ENABLE_FRAME_AWARE_REFRESH",
93            selective_refresh_enabled,
94            edited_module_count,
95            selected_diagnostic_instance_ids: selection.selected_diagnostic_instance_ids,
96            skipped_diagnostic_instance_ids: selection.skipped_diagnostic_instance_ids,
97            layer_marker: "frame-rule",
98        }
99    }
100
101    fn frame_refresh_graph_input(
102        &mut self,
103        modules: &[FrameAwareStyleModuleInputV0],
104    ) -> IncrementalGraphInputV0 {
105        self.revision = self.revision.saturating_add(1);
106        IncrementalGraphInputV0 {
107            revision: IncrementalRevisionV0 {
108                value: self.revision,
109            },
110            nodes: modules
111                .iter()
112                .map(|module| self.frame_refresh_node_input(module))
113                .collect(),
114        }
115    }
116
117    fn frame_refresh_node_input(
118        &mut self,
119        module: &FrameAwareStyleModuleInputV0,
120    ) -> IncrementalNodeInputV0 {
121        let cache = self
122            .parse_caches_by_module_id
123            .entry(module.module_id.clone())
124            .or_default();
125        let facts = summarize_omena_query_style_frame_refresh_facts_with_reuse(
126            module.source.as_str(),
127            module.dialect,
128            cache,
129        );
130        let dependency_ids = facts.dependency_ids.clone();
131        let digest = frame_refresh_digest(module, &facts);
132
133        IncrementalNodeInputV0 {
134            id: module.module_id.clone(),
135            digest,
136            dependency_ids,
137        }
138    }
139}
140
141pub fn refresh_diagnostics_with_frame(
142    frames: &[DiagnosticFrameFootprintV0],
143    edited_module_ids: Vec<String>,
144) -> FrameAwareRefreshReportV0 {
145    let selective_refresh_enabled = frame_aware_refresh_enabled_from_env();
146    refresh_diagnostics_with_frame_policy(frames, edited_module_ids, selective_refresh_enabled)
147}
148
149pub fn refresh_diagnostics_with_frame_policy(
150    frames: &[DiagnosticFrameFootprintV0],
151    edited_module_ids: Vec<String>,
152    selective_refresh_enabled: bool,
153) -> FrameAwareRefreshReportV0 {
154    let edited_module_count = edited_module_ids.len();
155    let selection = if selective_refresh_enabled {
156        select_recheck_set_for_module_ids(frames, edited_module_ids)
157    } else {
158        select_recheck_set_for_module_ids(frames, frames_to_module_ids(frames))
159    };
160
161    FrameAwareRefreshReportV0 {
162        schema_version: "0",
163        product: "omena-lsp-server.frame-aware-refresh",
164        feature_gate: "OMENA_LSP_ENABLE_FRAME_AWARE_REFRESH",
165        selective_refresh_enabled,
166        edited_module_count,
167        selected_diagnostic_instance_ids: selection.selected_diagnostic_instance_ids,
168        skipped_diagnostic_instance_ids: selection.skipped_diagnostic_instance_ids,
169        layer_marker: "frame-rule",
170    }
171}
172
173pub fn compare_frame_refresh_against_unconditional_baseline(
174    frames: &[DiagnosticFrameFootprintV0],
175    edited_module_ids: Vec<String>,
176) -> FrameAwareRefreshComparisonV0 {
177    let unconditional =
178        refresh_diagnostics_with_frame_policy(frames, edited_module_ids.clone(), false);
179    let selective = refresh_diagnostics_with_frame_policy(frames, edited_module_ids, true);
180    let unconditional_selected_count = unconditional.selected_diagnostic_instance_ids.len();
181    let selective_selected_count = selective.selected_diagnostic_instance_ids.len();
182    let selected_work_reduction_count =
183        unconditional_selected_count.saturating_sub(selective_selected_count);
184    let selected_work_reduction_ratio = if unconditional_selected_count == 0 {
185        0.0
186    } else {
187        selected_work_reduction_count as f64 / unconditional_selected_count as f64
188    };
189
190    FrameAwareRefreshComparisonV0 {
191        schema_version: "0",
192        product: "omena-lsp-server.frame-aware-refresh-comparison",
193        fixed_workspace_fixture: "m4-alpha-fixed-workspace-frame-refresh",
194        diagnostic_frame_count: frames.len(),
195        edited_module_count: unconditional.edited_module_count,
196        unconditional_selected_count,
197        selective_selected_count,
198        skipped_diagnostic_count: selective.skipped_diagnostic_instance_ids.len(),
199        selected_work_reduction_count,
200        selected_work_reduction_ratio,
201        measured_latency_proxy: "selected-diagnostic-work-count",
202        selective_refresh_enabled_by_default: frame_aware_refresh_enabled(false, false),
203        feature_gate: "OMENA_LSP_ENABLE_FRAME_AWARE_REFRESH",
204        disable_gate: "OMENA_LSP_DISABLE_FRAME_AWARE_REFRESH",
205        layer_marker: "frame-rule",
206    }
207}
208
209fn frames_to_module_ids(frames: &[DiagnosticFrameFootprintV0]) -> Vec<String> {
210    frames
211        .iter()
212        .flat_map(|frame| frame.evidence_module_ids.iter().cloned())
213        .collect()
214}
215
216fn select_recheck_set_for_module_ids(
217    frames: &[DiagnosticFrameFootprintV0],
218    module_ids: Vec<String>,
219) -> RecheckSelectionV0 {
220    let footprint = compute_edit_footprint(module_ids);
221    select_recheck_set(frames, &footprint)
222}
223
224fn frame_refresh_digest(
225    module: &FrameAwareStyleModuleInputV0,
226    facts: &OmenaQueryStyleFrameRefreshFactsV0,
227) -> String {
228    frame_refresh_stable_hash_hex(
229        format!(
230            "source={};tokens={};errors={};deps={}",
231            module.source,
232            facts.token_count,
233            facts.error_count,
234            facts.dependency_ids.join(",")
235        )
236        .as_bytes(),
237    )
238}
239
240fn frame_refresh_stable_hash_hex(bytes: &[u8]) -> String {
241    let mut hash = 0xcbf2_9ce4_8422_2325_u64;
242    for byte in bytes {
243        hash ^= u64::from(*byte);
244        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
245    }
246    format!("{hash:016x}")
247}
248
249fn frame_aware_refresh_enabled_from_env() -> bool {
250    frame_aware_refresh_enabled(
251        std::env::var_os("OMENA_LSP_ENABLE_FRAME_AWARE_REFRESH").is_some(),
252        std::env::var_os("OMENA_LSP_DISABLE_FRAME_AWARE_REFRESH").is_some(),
253    )
254}
255
256fn frame_aware_refresh_enabled(enable_requested: bool, disable_requested: bool) -> bool {
257    enable_requested && !disable_requested
258}
259
260#[cfg(test)]
261mod tests {
262    use omena_cascade::derive_frame_for_diagnostic;
263
264    use super::*;
265
266    #[test]
267    fn refresh_report_selects_only_intersecting_frames() {
268        let frame = derive_frame_for_diagnostic(
269            "missing-static-class",
270            "d1",
271            vec!["file:///workspace/a.module.css".to_string()],
272        );
273        let report = refresh_diagnostics_with_frame(
274            &[frame],
275            vec!["file:///workspace/a.module.css".to_string()],
276        );
277
278        assert_eq!(report.schema_version, "0");
279        assert_eq!(report.layer_marker, "frame-rule");
280        assert_eq!(report.feature_gate, "OMENA_LSP_ENABLE_FRAME_AWARE_REFRESH");
281        assert_eq!(report.selected_diagnostic_instance_ids, vec!["d1"]);
282    }
283
284    #[test]
285    fn refresh_report_preserves_full_refresh_when_disabled() {
286        let selected = derive_frame_for_diagnostic(
287            "missing-static-class",
288            "selected",
289            vec!["file:///workspace/a.module.css".to_string()],
290        );
291        let otherwise_skipped = derive_frame_for_diagnostic(
292            "missing-static-class",
293            "otherwise-skipped",
294            vec!["file:///workspace/b.module.css".to_string()],
295        );
296        let report = refresh_diagnostics_with_frame_policy(
297            &[selected, otherwise_skipped],
298            vec!["file:///workspace/a.module.css".to_string()],
299            false,
300        );
301
302        assert!(!report.selective_refresh_enabled);
303        assert_eq!(
304            report.selected_diagnostic_instance_ids,
305            vec!["selected", "otherwise-skipped"]
306        );
307        assert!(report.skipped_diagnostic_instance_ids.is_empty());
308    }
309
310    #[test]
311    fn refresh_policy_is_positive_opt_in() {
312        assert!(!frame_aware_refresh_enabled(false, false));
313        assert!(frame_aware_refresh_enabled(true, false));
314        assert!(!frame_aware_refresh_enabled(true, true));
315        assert!(!frame_aware_refresh_enabled(false, true));
316    }
317
318    #[test]
319    fn fixed_workspace_comparison_reports_work_reduction_when_enabled() {
320        let frames = (0..100)
321            .map(|index| {
322                derive_frame_for_diagnostic(
323                    "missing-static-class",
324                    format!("d{index}"),
325                    vec![format!("file:///workspace/module-{index}.module.css")],
326                )
327            })
328            .collect::<Vec<_>>();
329        let report = compare_frame_refresh_against_unconditional_baseline(
330            &frames,
331            vec!["file:///workspace/module-7.module.css".to_string()],
332        );
333
334        assert_eq!(report.schema_version, "0");
335        assert_eq!(report.layer_marker, "frame-rule");
336        assert_eq!(report.feature_gate, "OMENA_LSP_ENABLE_FRAME_AWARE_REFRESH");
337        assert!(!report.selective_refresh_enabled_by_default);
338        assert_eq!(report.diagnostic_frame_count, 100);
339        assert_eq!(report.unconditional_selected_count, 100);
340        assert_eq!(report.selective_selected_count, 1);
341        assert_eq!(report.skipped_diagnostic_count, 99);
342        assert!(report.selected_work_reduction_ratio >= 0.99);
343    }
344
345    #[test]
346    fn runtime_refresh_uses_parser_facts_and_salsa_dirty_dependencies() {
347        let mut runtime = FrameAwareRefreshRuntimeV0::default();
348        let frames = vec![
349            derive_frame_for_diagnostic(
350                "missing-static-class",
351                "a-diagnostic",
352                vec!["a".to_string()],
353            ),
354            derive_frame_for_diagnostic(
355                "missing-static-class",
356                "b-diagnostic",
357                vec!["b".to_string()],
358            ),
359        ];
360        let initial = vec![
361            FrameAwareStyleModuleInputV0 {
362                module_id: "a".to_string(),
363                source: ".a { color: red; }".to_string(),
364                dialect: OmenaParserStyleDialect::Scss,
365            },
366            FrameAwareStyleModuleInputV0 {
367                module_id: "b".to_string(),
368                source: "@use \"a\"; .b { color: blue; }".to_string(),
369                dialect: OmenaParserStyleDialect::Scss,
370            },
371        ];
372        let first = runtime.refresh_diagnostics_with_style_modules_policy(&frames, &initial, true);
373        assert_eq!(
374            first.selected_diagnostic_instance_ids,
375            vec!["a-diagnostic", "b-diagnostic"]
376        );
377
378        let changed = vec![
379            FrameAwareStyleModuleInputV0 {
380                module_id: "a".to_string(),
381                source: ".a { color: green; }".to_string(),
382                dialect: OmenaParserStyleDialect::Scss,
383            },
384            FrameAwareStyleModuleInputV0 {
385                module_id: "b".to_string(),
386                source: "@use \"a\"; .b { color: blue; }".to_string(),
387                dialect: OmenaParserStyleDialect::Scss,
388            },
389        ];
390        let second = runtime.refresh_diagnostics_with_style_modules_policy(&frames, &changed, true);
391
392        assert_eq!(second.edited_module_count, 2);
393        assert_eq!(
394            second.selected_diagnostic_instance_ids,
395            vec!["a-diagnostic", "b-diagnostic"]
396        );
397    }
398}