Skip to main content

copybook_governance_runtime/
lib.rs

1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Runtime governance evaluation for copybook-rs feature controls.
4//!
5//! This crate turns static governance mappings into runtime state by evaluating
6//! active feature flags against support-matrix rows.
7
8/// Re-exported feature flag types from the governance grid.
9pub mod feature_flags {
10    pub use copybook_governance_grid::feature_flags::*;
11}
12
13/// Re-exported support matrix types from the governance grid.
14pub mod support_matrix {
15    pub use copybook_governance_grid::support_matrix::*;
16}
17
18use serde::Serialize;
19
20pub use copybook_governance_grid::{
21    GovernanceSummary, GovernedFeatureBinding, feature_flags_for_support_id, governance_bindings,
22    summarize_governance,
23};
24pub use feature_flags::{
25    Feature, FeatureCategory, FeatureFlags, FeatureFlagsBuilder, FeatureFlagsHandle,
26    FeatureLifecycle,
27};
28pub use support_matrix::{FeatureId, FeatureSupport, SupportStatus};
29
30/// Runtime-aware governance state for a support matrix feature row.
31#[derive(Debug, Clone, Serialize)]
32#[non_exhaustive]
33pub struct FeatureGovernanceState {
34    /// Canonical support feature identifier from the support matrix.
35    pub support_id: FeatureId,
36    /// Human-readable support feature name.
37    pub support_name: &'static str,
38    /// Short support feature description.
39    pub support_description: &'static str,
40    /// Static support status from the support matrix.
41    pub support_status: SupportStatus,
42    /// Reference to canonical docs row, when available.
43    pub doc_ref: Option<&'static str>,
44    /// Why this support item is linked to feature flags.
45    pub rationale: &'static str,
46    /// Runtime flags required to express this feature in an environment.
47    pub required_feature_flags: &'static [Feature],
48    /// Runtime flags currently disabling this support item.
49    pub missing_feature_flags: Vec<Feature>,
50    /// Whether this support feature is currently fully enabled by active flags.
51    pub runtime_enabled: bool,
52}
53
54impl FeatureGovernanceState {
55    /// Build a non-governed runtime state row directly from static support metadata.
56    #[inline]
57    #[must_use]
58    pub fn from_support(feature: &'static FeatureSupport) -> Self {
59        Self {
60            support_id: feature.id,
61            support_name: feature.name,
62            support_description: feature.description,
63            support_status: feature.status,
64            doc_ref: feature.doc_ref,
65            rationale: "No runtime governance mapping requested.",
66            required_feature_flags: &[],
67            missing_feature_flags: Vec::new(),
68            runtime_enabled: true,
69        }
70    }
71}
72
73/// Summary of static + runtime governance coverage.
74#[derive(Debug, Clone, Copy, Serialize)]
75#[non_exhaustive]
76pub struct FeatureGovernanceSummary {
77    /// Total number of support-matrix feature entries.
78    pub total_support_features: usize,
79    /// Number of support features with at least one governance binding.
80    pub mapped_support_features: usize,
81    /// Total number of feature-flag bindings across all entries.
82    pub total_linked_feature_flags: usize,
83    /// Number of features that are currently enabled at runtime.
84    pub runtime_enabled_features: usize,
85    /// Number of features that are currently disabled at runtime.
86    pub runtime_disabled_features: usize,
87}
88
89impl FeatureGovernanceSummary {
90    /// Returns `true` when every support feature has at least one governance row.
91    #[inline]
92    #[must_use]
93    pub const fn all_support_rows_present(&self) -> bool {
94        self.total_support_features == self.mapped_support_features
95    }
96
97    /// Returns `true` when at least one feature is disabled at runtime.
98    #[inline]
99    #[must_use]
100    pub const fn has_runtime_unavailable_features(&self) -> bool {
101        self.runtime_disabled_features > 0
102    }
103}
104
105/// Return support rows without applying runtime governance mapping.
106#[inline]
107#[must_use]
108pub fn support_states() -> Vec<FeatureGovernanceState> {
109    support_matrix::all_features()
110        .iter()
111        .map(FeatureGovernanceState::from_support)
112        .collect()
113}
114
115/// Build full governance state rows for a specific runtime flag set.
116#[inline]
117#[must_use]
118pub fn governance_states(feature_flags: &FeatureFlags) -> Vec<FeatureGovernanceState> {
119    governance_bindings()
120        .iter()
121        .filter_map(|binding| governance_state_for_support_id(binding.support_id, feature_flags))
122        .collect()
123}
124
125/// Resolve runtime governance state for a single support item.
126#[inline]
127#[must_use]
128pub fn governance_state_for_support_id(
129    support_id: FeatureId,
130    feature_flags: &FeatureFlags,
131) -> Option<FeatureGovernanceState> {
132    let binding = governance_bindings()
133        .iter()
134        .find(|entry| entry.support_id == support_id)?;
135    let support = support_matrix::find_feature_by_id(support_id)?;
136
137    let mut missing_feature_flags = Vec::new();
138    for flag in binding.feature_flags {
139        if !feature_flags.is_enabled(*flag) {
140            missing_feature_flags.push(*flag);
141        }
142    }
143
144    let runtime_enabled = missing_feature_flags.is_empty();
145    Some(FeatureGovernanceState {
146        support_id,
147        support_name: support.name,
148        support_description: support.description,
149        support_status: support.status,
150        doc_ref: support.doc_ref,
151        rationale: binding.rationale,
152        required_feature_flags: binding.feature_flags,
153        missing_feature_flags,
154        runtime_enabled,
155    })
156}
157
158/// Evaluate whether a support feature is runtime-available with active flags.
159#[inline]
160#[must_use]
161pub fn is_support_runtime_available(support_id: FeatureId, feature_flags: &FeatureFlags) -> bool {
162    governance_state_for_support_id(support_id, feature_flags)
163        .is_some_and(|state| state.runtime_enabled)
164}
165
166/// Aggregate runtime availability with static governance coverage metadata.
167#[inline]
168#[must_use]
169pub fn runtime_summary(feature_flags: &FeatureFlags) -> FeatureGovernanceSummary {
170    let base = summarize_governance();
171    let states = governance_states(feature_flags);
172    let runtime_enabled_features = states.iter().filter(|state| state.runtime_enabled).count();
173
174    FeatureGovernanceSummary {
175        total_support_features: base.total_support_features,
176        mapped_support_features: base.mapped_support_features,
177        total_linked_feature_flags: base.total_linked_feature_flags,
178        runtime_enabled_features,
179        runtime_disabled_features: states.len().saturating_sub(runtime_enabled_features),
180    }
181}
182
183#[cfg(test)]
184#[allow(clippy::expect_used)]
185#[allow(clippy::unwrap_used)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_support_states_nonempty() {
191        let states = support_states();
192        assert!(!states.is_empty());
193        assert!(states.iter().all(|state| state.runtime_enabled));
194    }
195
196    #[test]
197    fn test_runtime_state_for_supported_feature() {
198        let flags = FeatureFlags::default();
199        let state = governance_state_for_support_id(FeatureId::Comp1Comp2, &flags)
200            .expect("state should exist for mapped feature");
201        assert_eq!(state.support_id, FeatureId::Comp1Comp2);
202        assert!(state.runtime_enabled);
203        assert_eq!(state.required_feature_flags.len(), 2);
204    }
205
206    #[test]
207    fn test_runtime_state_reports_missing_feature_flags() {
208        let flags = FeatureFlags::builder()
209            .disable(Feature::Comp1)
210            .disable(Feature::Comp2)
211            .build();
212        let state = governance_state_for_support_id(FeatureId::Comp1Comp2, &flags)
213            .expect("state should exist for mapped feature");
214        assert!(!state.runtime_enabled);
215        assert_eq!(state.missing_feature_flags.len(), 2);
216    }
217
218    #[test]
219    fn test_runtime_summary_counts() {
220        let flags = FeatureFlags::builder()
221            .disable(Feature::SignSeparate)
222            .build();
223        let summary = runtime_summary(&flags);
224        assert!(summary.total_support_features >= 1);
225        assert!(summary.mapped_support_features <= summary.total_support_features);
226        assert!(summary.runtime_enabled_features <= summary.total_support_features);
227    }
228
229    #[test]
230    fn test_is_support_runtime_available() {
231        let flags = FeatureFlags::builder()
232            .enable(Feature::SignSeparate)
233            .disable(Feature::Comp1)
234            .build();
235        assert!(is_support_runtime_available(
236            FeatureId::SignSeparate,
237            &flags
238        ));
239        assert!(!is_support_runtime_available(FeatureId::Comp1Comp2, &flags));
240    }
241
242    #[test]
243    fn test_support_id_lookup() {
244        assert!(support_matrix::find_feature_by_id(FeatureId::Level88Conditions).is_some());
245        assert!(support_matrix::find_feature_by_id(FeatureId::NestedOdo).is_some());
246        assert!(support_matrix::find_feature_by_id(FeatureId::SignSeparate).is_some());
247    }
248
249    #[test]
250    fn test_support_states_count_matches_all_features() {
251        let states = support_states();
252        assert_eq!(states.len(), support_matrix::all_features().len());
253    }
254
255    #[test]
256    fn test_support_states_all_have_empty_required_flags() {
257        for state in support_states() {
258            assert!(
259                state.required_feature_flags.is_empty(),
260                "support_states should have no required flags for {:?}",
261                state.support_id
262            );
263            assert!(state.missing_feature_flags.is_empty());
264        }
265    }
266
267    #[test]
268    fn test_governance_states_with_all_defaults() {
269        let flags = FeatureFlags::default();
270        let states = governance_states(&flags);
271        assert!(!states.is_empty());
272        // With defaults, SignSeparate/Comp1/Comp2 are enabled, RenamesR4R6 is not
273        let renames_state = states
274            .iter()
275            .find(|s| s.support_id == FeatureId::Level66Renames)
276            .expect("Level66Renames should be in governance states");
277        assert!(!renames_state.runtime_enabled);
278    }
279
280    #[test]
281    fn test_governance_states_with_all_features_enabled() {
282        let mut flags = FeatureFlags::default();
283        for feature in feature_flags::all_features() {
284            flags.enable(feature);
285        }
286        let states = governance_states(&flags);
287        for state in &states {
288            assert!(
289                state.runtime_enabled,
290                "{:?} should be runtime enabled when all flags on",
291                state.support_id
292            );
293            assert!(state.missing_feature_flags.is_empty());
294        }
295    }
296
297    #[test]
298    fn test_governance_states_with_all_features_disabled() {
299        let mut flags = FeatureFlags::default();
300        for feature in feature_flags::all_features() {
301            flags.disable(feature);
302        }
303        let states = governance_states(&flags);
304        // Features with non-empty required_feature_flags should be disabled
305        for state in &states {
306            if !state.required_feature_flags.is_empty() {
307                assert!(
308                    !state.runtime_enabled,
309                    "{:?} should be disabled when all flags off",
310                    state.support_id
311                );
312            }
313        }
314    }
315
316    #[test]
317    fn test_runtime_summary_with_all_enabled() {
318        let mut flags = FeatureFlags::default();
319        for feature in feature_flags::all_features() {
320            flags.enable(feature);
321        }
322        let summary = runtime_summary(&flags);
323        assert!(!summary.has_runtime_unavailable_features());
324        assert_eq!(summary.runtime_disabled_features, 0);
325        assert_eq!(
326            summary.runtime_enabled_features,
327            summary.mapped_support_features
328        );
329    }
330
331    #[test]
332    fn test_runtime_summary_all_support_rows_present() {
333        let flags = FeatureFlags::default();
334        let summary = runtime_summary(&flags);
335        assert!(summary.all_support_rows_present());
336        assert_eq!(summary.total_support_features, 7);
337        assert_eq!(summary.mapped_support_features, 7);
338    }
339
340    #[test]
341    fn test_from_support_sets_no_governance_rationale() {
342        let feature = support_matrix::find_feature_by_id(FeatureId::EditedPic).unwrap();
343        let state = FeatureGovernanceState::from_support(feature);
344        assert_eq!(state.rationale, "No runtime governance mapping requested.");
345        assert!(state.runtime_enabled);
346        assert!(state.required_feature_flags.is_empty());
347        assert!(state.missing_feature_flags.is_empty());
348    }
349
350    #[test]
351    fn test_governance_state_for_support_id_preserves_metadata() {
352        let flags = FeatureFlags::default();
353        let state = governance_state_for_support_id(FeatureId::SignSeparate, &flags).unwrap();
354        assert_eq!(state.support_id, FeatureId::SignSeparate);
355        assert!(!state.support_name.is_empty());
356        assert!(!state.support_description.is_empty());
357        assert!(state.doc_ref.is_some());
358        assert!(!state.rationale.is_empty());
359    }
360
361    #[test]
362    fn test_is_support_runtime_available_for_ungoverned_feature() {
363        // Level88 has no feature flags, so it's always available
364        let flags = FeatureFlags::default();
365        let state = governance_state_for_support_id(FeatureId::Level88Conditions, &flags).unwrap();
366        assert!(state.runtime_enabled);
367        assert!(state.required_feature_flags.is_empty());
368    }
369
370    #[test]
371    fn test_runtime_summary_disabled_count_matches_expectations() {
372        let flags = FeatureFlags::builder()
373            .disable(Feature::SignSeparate)
374            .disable(Feature::Comp1)
375            .disable(Feature::Comp2)
376            .build();
377        let summary = runtime_summary(&flags);
378        // SignSeparate disabled -> SignSeparate row disabled
379        // Comp1+Comp2 disabled -> Comp1Comp2 row disabled
380        // RenamesR4R6 not default-enabled -> Level66Renames row disabled
381        assert!(summary.runtime_disabled_features >= 3);
382        assert!(summary.has_runtime_unavailable_features());
383    }
384}