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}