1use std::collections::BTreeSet;
7
8use omena_syntax::ident::AuthoredPropertyTextV0;
9use serde::Serialize;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
12#[serde(rename_all = "camelCase")]
13pub struct DiagnosticFrameFootprintV0 {
14 pub schema_version: &'static str,
15 pub product: &'static str,
16 pub feature_gate: &'static str,
17 pub diagnostic_code: String,
18 pub diagnostic_instance_id: String,
19 pub evidence_module_ids: Vec<String>,
20 pub resolver_evidence: Vec<ResolverEvidenceV0>,
21 pub cascade_evidence: Vec<CascadeEvidenceV0>,
22 pub custom_property_evidence: Vec<CustomPropertyEvidenceV0>,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub outcome_conjunction_witness: Option<OutcomeConjunctionWitnessV0>,
25 pub conservative: bool,
26 pub layer_marker: &'static str,
27}
28
29#[derive(Debug, Clone, Serialize)]
30#[serde(rename_all = "camelCase")]
31pub struct CascadeEvidenceV0 {
32 pub selector: String,
33 pub property: AuthoredPropertyTextV0,
34 pub declaration_ids: Vec<String>,
35}
36
37impl PartialEq for CascadeEvidenceV0 {
38 fn eq(&self, other: &Self) -> bool {
39 self.selector == other.selector
40 && self.property.to_property_name().canonical_key()
41 == other.property.to_property_name().canonical_key()
42 && self.declaration_ids == other.declaration_ids
43 }
44}
45
46impl Eq for CascadeEvidenceV0 {}
47
48#[derive(Debug, Clone, Serialize)]
49#[serde(rename_all = "camelCase")]
50pub struct CustomPropertyEvidenceV0 {
51 pub custom_property_name: AuthoredPropertyTextV0,
52 pub dependency_names: Vec<AuthoredPropertyTextV0>,
53}
54
55impl PartialEq for CustomPropertyEvidenceV0 {
56 fn eq(&self, other: &Self) -> bool {
57 self.custom_property_name.to_custom_key() == other.custom_property_name.to_custom_key()
58 && self.dependency_names.len() == other.dependency_names.len()
59 && self
60 .dependency_names
61 .iter()
62 .zip(&other.dependency_names)
63 .all(|(left, right)| left.to_custom_key() == right.to_custom_key())
64 }
65}
66
67impl Eq for CustomPropertyEvidenceV0 {}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
70#[serde(rename_all = "camelCase")]
71pub struct ResolverEvidenceV0 {
72 pub specifier: String,
73 pub resolved_module_id: String,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
77#[serde(rename_all = "camelCase")]
78pub struct OutcomeConjunctionWitnessV0 {
79 pub schema_version: &'static str,
80 pub product: &'static str,
81 pub layer_marker: &'static str,
82 pub feature_gate: &'static str,
83 pub partition_id: String,
84 pub outcome_key_count: usize,
85 pub conservative: bool,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
89#[serde(rename_all = "camelCase")]
90pub struct ModuleFootprintV0 {
91 pub schema_version: &'static str,
92 pub product: &'static str,
93 pub layer_marker: &'static str,
94 pub feature_gate: &'static str,
95 pub module_ids: Vec<String>,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
99#[serde(rename_all = "camelCase")]
100pub struct RecheckSelectionV0 {
101 pub schema_version: &'static str,
102 pub product: &'static str,
103 pub layer_marker: &'static str,
104 pub feature_gate: &'static str,
105 pub selected_diagnostic_instance_ids: Vec<String>,
106 pub skipped_diagnostic_instance_ids: Vec<String>,
107 pub conservative: bool,
108}
109
110pub fn derive_frame_for_diagnostic(
111 diagnostic_code: impl Into<String>,
112 diagnostic_instance_id: impl Into<String>,
113 evidence_module_ids: Vec<String>,
114) -> DiagnosticFrameFootprintV0 {
115 let evidence_module_ids = canonicalize_module_ids(evidence_module_ids);
116 DiagnosticFrameFootprintV0 {
117 schema_version: "0",
118 product: "omena-cascade.diagnostic-frame-footprint",
119 feature_gate: "frame-rule",
120 diagnostic_code: diagnostic_code.into(),
121 diagnostic_instance_id: diagnostic_instance_id.into(),
122 resolver_evidence: evidence_module_ids
123 .iter()
124 .map(|module_id| ResolverEvidenceV0 {
125 specifier: module_id.clone(),
126 resolved_module_id: module_id.clone(),
127 })
128 .collect(),
129 cascade_evidence: Vec::new(),
130 custom_property_evidence: Vec::new(),
131 outcome_conjunction_witness: Some(outcome_conjunction_witness(&evidence_module_ids)),
132 evidence_module_ids,
133 conservative: true,
134 layer_marker: "frame-rule",
135 }
136}
137
138pub fn derive_frames_for_diagnostic_set(
139 diagnostics: Vec<(String, String, Vec<String>)>,
140) -> Vec<DiagnosticFrameFootprintV0> {
141 diagnostics
142 .into_iter()
143 .map(|(code, instance_id, module_ids)| {
144 derive_frame_for_diagnostic(code, instance_id, module_ids)
145 })
146 .collect()
147}
148
149pub fn compute_edit_footprint(module_ids: Vec<String>) -> ModuleFootprintV0 {
150 ModuleFootprintV0 {
151 schema_version: "0",
152 product: "omena-cascade.module-footprint",
153 layer_marker: "frame-rule",
154 feature_gate: "frame-rule",
155 module_ids: canonicalize_module_ids(module_ids),
156 }
157}
158
159pub fn select_recheck_set(
160 frames: &[DiagnosticFrameFootprintV0],
161 edit_footprint: &ModuleFootprintV0,
162) -> RecheckSelectionV0 {
163 let edit_modules = edit_footprint
164 .module_ids
165 .iter()
166 .collect::<BTreeSet<&String>>();
167 let mut selected = Vec::new();
168 let mut skipped = Vec::new();
169
170 for frame in frames {
171 if frame
172 .evidence_module_ids
173 .iter()
174 .any(|module_id| edit_modules.contains(module_id))
175 {
176 selected.push(frame.diagnostic_instance_id.clone());
177 } else {
178 skipped.push(frame.diagnostic_instance_id.clone());
179 }
180 }
181
182 RecheckSelectionV0 {
183 schema_version: "0",
184 product: "omena-cascade.recheck-selection",
185 layer_marker: "frame-rule",
186 feature_gate: "frame-rule",
187 selected_diagnostic_instance_ids: selected,
188 skipped_diagnostic_instance_ids: skipped,
189 conservative: true,
190 }
191}
192
193pub fn intersect_frame_with_footprint(
194 frame: &DiagnosticFrameFootprintV0,
195 footprint: &ModuleFootprintV0,
196) -> bool {
197 let module_ids = footprint.module_ids.iter().collect::<BTreeSet<&String>>();
198 frame
199 .evidence_module_ids
200 .iter()
201 .any(|module_id| module_ids.contains(module_id))
202}
203
204pub fn outcome_conjunction_witness(module_ids: &[String]) -> OutcomeConjunctionWitnessV0 {
205 OutcomeConjunctionWitnessV0 {
206 schema_version: "0",
207 product: "omena-cascade.outcome-conjunction-witness",
208 layer_marker: "frame-rule",
209 feature_gate: "frame-rule",
210 partition_id: module_ids.join("+"),
211 outcome_key_count: module_ids.len(),
212 conservative: true,
213 }
214}
215
216pub fn partition_into_outcome_conjunction_classes(
217 frames: &[DiagnosticFrameFootprintV0],
218) -> Vec<OutcomeConjunctionWitnessV0> {
219 frames
220 .iter()
221 .filter_map(|frame| frame.outcome_conjunction_witness.clone())
222 .collect()
223}
224
225fn canonicalize_module_ids(module_ids: Vec<String>) -> Vec<String> {
226 module_ids
227 .into_iter()
228 .collect::<BTreeSet<_>>()
229 .into_iter()
230 .collect()
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn frame_selection_is_sorted_deduped_and_conservative() {
239 let frame = derive_frame_for_diagnostic(
240 "missing-static-class",
241 "d1",
242 vec!["b".into(), "a".into(), "a".into()],
243 );
244 let footprint = compute_edit_footprint(vec!["a".into()]);
245 let selection = select_recheck_set(std::slice::from_ref(&frame), &footprint);
246
247 assert_eq!(frame.evidence_module_ids, vec!["a", "b"]);
248 assert_eq!(frame.feature_gate, "frame-rule");
249 assert_eq!(footprint.feature_gate, "frame-rule");
250 assert_eq!(selection.layer_marker, "frame-rule");
251 assert_eq!(selection.feature_gate, "frame-rule");
252 assert!(
253 frame
254 .outcome_conjunction_witness
255 .as_ref()
256 .is_some_and(|witness| witness.feature_gate == "frame-rule")
257 );
258 assert!(frame.conservative);
259 assert!(intersect_frame_with_footprint(&frame, &footprint));
260 assert_eq!(selection.selected_diagnostic_instance_ids, vec!["d1"]);
261 }
262}