Skip to main content

adze_governance_metadata/
lib.rs

1//! Shared governance metadata building blocks for BDD scenario progress and parser feature profiles.
2//!
3//! This crate intentionally owns the small, reusable metadata types used by build-time
4//! artifacts and runtime dashboards.
5
6#![forbid(unsafe_op_in_unsafe_fn)]
7#![deny(missing_docs)]
8#![cfg_attr(feature = "strict_api", deny(unreachable_pub))]
9#![cfg_attr(not(feature = "strict_api"), warn(unreachable_pub))]
10#![cfg_attr(feature = "strict_docs", deny(missing_docs))]
11#![cfg_attr(not(feature = "strict_docs"), allow(missing_docs))]
12
13use adze_bdd_grid_core::{BddPhase, BddScenario, bdd_progress};
14use adze_feature_policy_core::{ParserBackend, ParserFeatureProfile};
15use serde::{Deserialize, Serialize};
16
17/// Snapshot of parser feature flags captured in build artifacts and diagnostics.
18///
19/// # Examples
20///
21/// ```
22/// use adze_governance_metadata::ParserFeatureProfileSnapshot;
23///
24/// let snap = ParserFeatureProfileSnapshot::new(true, false, true, false);
25/// assert!(snap.pure_rust);
26/// assert!(snap.tree_sitter_c2rust);
27/// assert!(!snap.glr);
28/// ```
29#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
30pub struct ParserFeatureProfileSnapshot {
31    /// Pure-rust mode flag.
32    pub pure_rust: bool,
33    /// `tree-sitter-standard` feature flag.
34    pub tree_sitter_standard: bool,
35    /// `tree-sitter-c2rust` feature flag.
36    pub tree_sitter_c2rust: bool,
37    /// GLR feature flag.
38    pub glr: bool,
39}
40
41impl ParserFeatureProfileSnapshot {
42    /// Create a snapshot from explicit parser feature flags.
43    ///
44    /// # Examples
45    ///
46    /// ```
47    /// use adze_governance_metadata::ParserFeatureProfileSnapshot;
48    ///
49    /// let snap = ParserFeatureProfileSnapshot::new(false, true, false, true);
50    /// assert!(!snap.pure_rust);
51    /// assert!(snap.tree_sitter_standard);
52    /// assert!(snap.glr);
53    /// ```
54    #[must_use]
55    pub const fn new(
56        pure_rust: bool,
57        tree_sitter_standard: bool,
58        tree_sitter_c2rust: bool,
59        glr: bool,
60    ) -> Self {
61        Self {
62            pure_rust,
63            tree_sitter_standard,
64            tree_sitter_c2rust,
65            glr,
66        }
67    }
68
69    /// Create a snapshot from the parser-profile contract.
70    ///
71    /// # Examples
72    ///
73    /// ```
74    /// use adze_governance_metadata::ParserFeatureProfileSnapshot;
75    /// use adze_feature_policy_core::ParserFeatureProfile;
76    ///
77    /// let profile = ParserFeatureProfile {
78    ///     pure_rust: true, tree_sitter_standard: false,
79    ///     tree_sitter_c2rust: false, glr: true,
80    /// };
81    /// let snap = ParserFeatureProfileSnapshot::from_profile(profile);
82    /// assert!(snap.pure_rust);
83    /// assert!(snap.glr);
84    /// ```
85    #[must_use]
86    pub const fn from_profile(profile: ParserFeatureProfile) -> Self {
87        Self {
88            pure_rust: profile.pure_rust,
89            tree_sitter_standard: profile.tree_sitter_standard,
90            tree_sitter_c2rust: profile.tree_sitter_c2rust,
91            glr: profile.glr,
92        }
93    }
94
95    /// Resolve an equivalent parser-profile from this snapshot.
96    #[must_use]
97    pub const fn as_profile(self) -> ParserFeatureProfile {
98        ParserFeatureProfile {
99            pure_rust: self.pure_rust,
100            tree_sitter_standard: self.tree_sitter_standard,
101            tree_sitter_c2rust: self.tree_sitter_c2rust,
102            glr: self.glr,
103        }
104    }
105
106    /// Build a snapshot from Cargo feature environment variables.
107    #[must_use]
108    pub fn from_env() -> Self {
109        Self {
110            pure_rust: env_flag(&["CARGO_FEATURE_PURE_RUST", "ADZE_USE_PURE_RUST"]),
111            tree_sitter_standard: env_flag(&["CARGO_FEATURE_TREE_SITTER_STANDARD"]),
112            tree_sitter_c2rust: env_flag(&["CARGO_FEATURE_TREE_SITTER_C2RUST"]),
113            glr: env_flag(&["CARGO_FEATURE_GLR"]),
114        }
115    }
116
117    /// Return the non-conflict backend name implied by this profile.
118    #[must_use]
119    pub const fn non_conflict_backend(self) -> &'static str {
120        if self.glr {
121            ParserBackend::GLR.name()
122        } else if self.pure_rust {
123            ParserBackend::PureRust.name()
124        } else {
125            ParserBackend::TreeSitter.name()
126        }
127    }
128
129    /// Resolve the non-conflict backend for this profile.
130    #[must_use]
131    pub const fn resolve_non_conflict_backend(self) -> ParserBackend {
132        self.as_profile().resolve_backend(false)
133    }
134
135    /// Resolve backend selection for a grammar with conflicts.
136    #[must_use]
137    pub const fn resolve_conflict_backend(self) -> ParserBackend {
138        self.as_profile().resolve_backend(true)
139    }
140}
141
142fn env_flag(names: &[&str]) -> bool {
143    names.iter().any(|name| std::env::var(name).is_ok())
144}
145
146/// BDD governance metadata embedded in generated parse artifacts.
147///
148/// # Examples
149///
150/// ```
151/// use adze_governance_metadata::GovernanceMetadata;
152///
153/// let meta = GovernanceMetadata::with_counts("core", 5, 8, "core:5/8");
154/// assert_eq!(meta.implemented, 5);
155/// assert_eq!(meta.total, 8);
156/// assert!(!meta.is_complete());
157/// ```
158#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
159pub struct GovernanceMetadata {
160    /// Phase label for this BDD snapshot.
161    pub phase: String,
162    /// Implemented scenario count.
163    pub implemented: usize,
164    /// Total scenario count.
165    pub total: usize,
166    /// Stable status line for dashboards.
167    pub status_line: String,
168}
169
170impl GovernanceMetadata {
171    /// Whether all known scenarios are complete.
172    ///
173    /// # Examples
174    ///
175    /// ```
176    /// use adze_governance_metadata::GovernanceMetadata;
177    ///
178    /// let done = GovernanceMetadata::with_counts("rt", 8, 8, "rt:8/8");
179    /// assert!(done.is_complete());
180    ///
181    /// let wip = GovernanceMetadata::with_counts("rt", 3, 8, "rt:3/8");
182    /// assert!(!wip.is_complete());
183    /// ```
184    #[must_use]
185    pub fn is_complete(&self) -> bool {
186        self.total > 0 && self.implemented == self.total
187    }
188
189    /// Construct a governance snapshot from explicit counts.
190    ///
191    /// # Examples
192    ///
193    /// ```
194    /// use adze_governance_metadata::GovernanceMetadata;
195    ///
196    /// let meta = GovernanceMetadata::with_counts("runtime", 6, 8, "runtime:6/8");
197    /// assert_eq!(meta.phase, "runtime");
198    /// assert_eq!(meta.implemented, 6);
199    /// ```
200    #[must_use]
201    pub fn with_counts(
202        phase: impl Into<String>,
203        implemented: usize,
204        total: usize,
205        status_line: impl Into<String>,
206    ) -> Self {
207        Self {
208            phase: phase.into(),
209            implemented,
210            total,
211            status_line: status_line.into(),
212        }
213    }
214
215    /// Build metadata from a BDD scenario grid and parser feature profile.
216    #[must_use]
217    pub fn for_grid(
218        phase: BddPhase,
219        scenarios: &[BddScenario],
220        profile: ParserFeatureProfile,
221    ) -> Self {
222        let (implemented, total) = bdd_progress(phase, scenarios);
223        Self {
224            phase: phase_name(phase).to_string(),
225            implemented,
226            total,
227            status_line: status_line(phase, implemented, total, profile),
228        }
229    }
230}
231
232impl Default for GovernanceMetadata {
233    fn default() -> Self {
234        Self {
235            phase: "runtime".to_string(),
236            implemented: 0,
237            total: 0,
238            status_line: "runtime:0/0".to_string(),
239        }
240    }
241}
242
243fn phase_name(phase: BddPhase) -> &'static str {
244    match phase {
245        BddPhase::Core => "core",
246        BddPhase::Runtime => "runtime",
247    }
248}
249
250fn status_line(
251    phase: BddPhase,
252    implemented: usize,
253    total: usize,
254    profile: ParserFeatureProfile,
255) -> String {
256    let backend = profile.resolve_backend(false).name();
257    let phase_label = phase_name(phase);
258    format!("{phase_label}:{implemented}/{total}:{backend}:{profile}")
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn profile_snapshot_new() {
267        let snap = ParserFeatureProfileSnapshot::new(true, false, true, false);
268        assert!(snap.pure_rust);
269        assert!(!snap.tree_sitter_standard);
270        assert!(snap.tree_sitter_c2rust);
271        assert!(!snap.glr);
272    }
273
274    #[test]
275    fn profile_snapshot_roundtrip_via_profile() {
276        let snap = ParserFeatureProfileSnapshot::new(false, true, false, true);
277        let profile = snap.as_profile();
278        let snap2 = ParserFeatureProfileSnapshot::from_profile(profile);
279        assert_eq!(snap, snap2);
280    }
281
282    #[test]
283    fn profile_snapshot_serde_roundtrip() {
284        let snap = ParserFeatureProfileSnapshot::new(true, false, true, true);
285        let json = serde_json::to_string(&snap).unwrap();
286        let deserialized: ParserFeatureProfileSnapshot = serde_json::from_str(&json).unwrap();
287        assert_eq!(snap, deserialized);
288    }
289
290    #[test]
291    fn non_conflict_backend_name_is_non_empty() {
292        let snap = ParserFeatureProfileSnapshot::new(false, false, false, false);
293        assert!(!snap.non_conflict_backend().is_empty());
294    }
295
296    #[test]
297    fn governance_metadata_default() {
298        let meta = GovernanceMetadata::default();
299        assert_eq!(meta.phase, "runtime");
300        assert_eq!(meta.implemented, 0);
301        assert_eq!(meta.total, 0);
302        assert!(!meta.is_complete());
303    }
304
305    #[test]
306    fn governance_metadata_with_counts() {
307        let meta = GovernanceMetadata::with_counts("core", 5, 10, "core:5/10");
308        assert_eq!(meta.implemented, 5);
309        assert_eq!(meta.total, 10);
310        assert!(!meta.is_complete());
311    }
312
313    #[test]
314    fn governance_metadata_complete() {
315        let meta = GovernanceMetadata::with_counts("core", 8, 8, "done");
316        assert!(meta.is_complete());
317    }
318
319    #[test]
320    fn governance_metadata_serde_roundtrip() {
321        let meta = GovernanceMetadata::with_counts("runtime", 3, 7, "runtime:3/7");
322        let json = serde_json::to_string(&meta).unwrap();
323        let deserialized: GovernanceMetadata = serde_json::from_str(&json).unwrap();
324        assert_eq!(meta, deserialized);
325    }
326
327    #[test]
328    fn governance_metadata_for_grid() {
329        use adze_bdd_grid_core::BddScenarioStatus;
330        let scenarios = [BddScenario {
331            id: 1,
332            title: "test",
333            reference: "T-1",
334            core_status: BddScenarioStatus::Implemented,
335            runtime_status: BddScenarioStatus::Deferred { reason: "wip" },
336        }];
337        let profile = ParserFeatureProfile::current();
338        let meta = GovernanceMetadata::for_grid(BddPhase::Core, &scenarios, profile);
339        assert_eq!(meta.phase, "core");
340        assert_eq!(meta.total, 1);
341        assert_eq!(meta.implemented, 1);
342        assert!(!meta.status_line.is_empty());
343    }
344
345    #[test]
346    fn non_conflict_backend_glr_profile() {
347        let snap = ParserFeatureProfileSnapshot::new(true, false, false, true);
348        assert_eq!(snap.non_conflict_backend(), ParserBackend::GLR.name());
349    }
350
351    #[test]
352    fn non_conflict_backend_pure_rust_profile() {
353        let snap = ParserFeatureProfileSnapshot::new(true, false, false, false);
354        assert_eq!(snap.non_conflict_backend(), ParserBackend::PureRust.name());
355    }
356
357    #[test]
358    fn non_conflict_backend_tree_sitter_fallback() {
359        let snap = ParserFeatureProfileSnapshot::new(false, true, false, false);
360        assert_eq!(
361            snap.non_conflict_backend(),
362            ParserBackend::TreeSitter.name()
363        );
364    }
365
366    #[test]
367    fn resolve_non_conflict_and_conflict_backend() {
368        let snap = ParserFeatureProfileSnapshot::new(true, false, false, true);
369        let non_conflict = snap.resolve_non_conflict_backend();
370        let conflict = snap.resolve_conflict_backend();
371        assert_eq!(non_conflict, snap.as_profile().resolve_backend(false));
372        assert_eq!(conflict, snap.as_profile().resolve_backend(true));
373    }
374
375    #[test]
376    fn profile_snapshot_debug_format() {
377        let snap = ParserFeatureProfileSnapshot::new(true, false, true, false);
378        let debug = format!("{:?}", snap);
379        assert!(debug.contains("ParserFeatureProfileSnapshot"));
380        assert!(debug.contains("pure_rust: true"));
381    }
382
383    #[test]
384    fn profile_snapshot_hash_consistency() {
385        use std::collections::HashSet;
386        let a = ParserFeatureProfileSnapshot::new(true, false, true, false);
387        let b = ParserFeatureProfileSnapshot::new(true, false, true, false);
388        let c = ParserFeatureProfileSnapshot::new(false, false, true, false);
389        let mut set = HashSet::new();
390        set.insert(a);
391        set.insert(b);
392        set.insert(c);
393        assert_eq!(set.len(), 2);
394    }
395
396    #[test]
397    fn profile_snapshot_copy_semantics() {
398        let snap = ParserFeatureProfileSnapshot::new(true, false, true, false);
399        let copied = snap;
400        assert_eq!(snap, copied);
401    }
402
403    #[test]
404    fn from_env_with_no_vars() {
405        // With no governance env vars set, all fields should be false
406        // (unless actual Cargo features are active)
407        let snap = ParserFeatureProfileSnapshot::from_env();
408        let _ = snap.non_conflict_backend();
409        // Just verify it doesn't panic
410    }
411
412    #[test]
413    fn governance_metadata_clone_eq() {
414        let meta = GovernanceMetadata::with_counts("core", 3, 5, "core:3/5");
415        let cloned = meta.clone();
416        assert_eq!(meta, cloned);
417    }
418
419    #[test]
420    fn governance_metadata_for_grid_runtime_phase() {
421        use adze_bdd_grid_core::BddScenarioStatus;
422        let scenarios = [BddScenario {
423            id: 1,
424            title: "test",
425            reference: "T-1",
426            core_status: BddScenarioStatus::Deferred { reason: "later" },
427            runtime_status: BddScenarioStatus::Implemented,
428        }];
429        let profile = ParserFeatureProfile::current();
430        let meta = GovernanceMetadata::for_grid(BddPhase::Runtime, &scenarios, profile);
431        assert_eq!(meta.phase, "runtime");
432        assert_eq!(meta.implemented, 1);
433        assert_eq!(meta.total, 1);
434    }
435
436    #[test]
437    fn governance_metadata_for_grid_empty_scenarios() {
438        let profile = ParserFeatureProfile::current();
439        let meta = GovernanceMetadata::for_grid(BddPhase::Core, &[], profile);
440        assert_eq!(meta.implemented, 0);
441        assert_eq!(meta.total, 0);
442    }
443
444    #[test]
445    fn governance_metadata_debug_format() {
446        let meta = GovernanceMetadata::with_counts("core", 1, 2, "core:1/2");
447        let debug = format!("{:?}", meta);
448        assert!(debug.contains("GovernanceMetadata"));
449        assert!(debug.contains("core"));
450    }
451}