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