Skip to main content

omena_cascade/
frame_footprint.rs

1//! Frame-aware diagnostic recheck contracts for incremental cascade consumers.
2//!
3//! This module exposes the conservative V0 footprint records used to decide
4//! which diagnostics must be rechecked after a bounded module edit.
5
6use 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}