Skip to main content

rch_common/
schema_versions.rs

1//! Central registry of schema versions across the workspace.
2//!
3//! Why this exists: before this module, the four schema-version constants
4//! lived independently in their owning crates. They all happened to be
5//! `"1.0.0"`, which masked a real bug — a comparison that should have
6//! checked compatibility between two of them silently passed because the
7//! values coincided. (Fixed in commit `68fcb7c`.)
8//!
9//! Going forward, every schema-version constant in the workspace is
10//! sourced from this registry. The pinned-snapshot test in this module
11//! is the gate: bumping a version requires intentionally updating the
12//! snapshot table, which surfaces the change in code review.
13//!
14//! # Components covered
15//!
16//! | `SchemaComponent` variant      | Owning surface                              |
17//! |--------------------------------|---------------------------------------------|
18//! | `DoctorReliability`            | `rch doctor --reliability` JSON envelope    |
19//! | `Status`                       | `rch status --json` envelope                |
20//! | `RepoUpdaterContract`          | repo-updater protocol over the wire         |
21//! | `ProcessTriageContract`        | process-triage protocol over the wire       |
22//!
23//! # Bump policy
24//!
25//! See bead `remote_compilation_helper-62u24.11` "Schema-version bump
26//! policy + migration playbook" for the canonical procedure. In short:
27//! - MAJOR: removal or rename (breaking)
28//! - MINOR: new field or new enum variant (additive)
29//! - PATCH: serialization-order change or doc-only change affecting
30//!   golden tests
31//!
32//! When you bump, ALSO update [`tests::test_schema_versions_match_snapshot`].
33//! Reviewers see the snapshot delta and confirm the rationale.
34
35use serde::{Deserialize, Serialize};
36
37/// Stable identifier for each component that exposes a versioned schema.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
39#[serde(rename_all = "snake_case")]
40pub enum SchemaComponent {
41    /// `rch doctor --reliability` response envelope.
42    DoctorReliability,
43    /// `rch status --json` response envelope.
44    Status,
45    /// Repo-updater wire contract.
46    RepoUpdaterContract,
47    /// Process-triage wire contract.
48    ProcessTriageContract,
49}
50
51/// The canonical version string for a given component.
52///
53/// `const fn` so callers can use the result in `pub const` declarations
54/// (preserves the existing call sites that read into compile-time
55/// constants).
56#[must_use]
57pub const fn current_version(component: SchemaComponent) -> &'static str {
58    match component {
59        SchemaComponent::DoctorReliability => "1.0.0",
60        SchemaComponent::Status => "1.0.0",
61        SchemaComponent::RepoUpdaterContract => "1.0.0",
62        SchemaComponent::ProcessTriageContract => "1.0.0",
63    }
64}
65
66/// Every component, paired with its current version. Iterable in tests
67/// and at runtime.
68pub const ALL_COMPONENTS: &[(SchemaComponent, &str)] = &[
69    (
70        SchemaComponent::DoctorReliability,
71        current_version(SchemaComponent::DoctorReliability),
72    ),
73    (
74        SchemaComponent::Status,
75        current_version(SchemaComponent::Status),
76    ),
77    (
78        SchemaComponent::RepoUpdaterContract,
79        current_version(SchemaComponent::RepoUpdaterContract),
80    ),
81    (
82        SchemaComponent::ProcessTriageContract,
83        current_version(SchemaComponent::ProcessTriageContract),
84    ),
85];
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    /// Pinned snapshot of every component's version. Bumping a version
92    /// in [`current_version`] without updating this table fails the test —
93    /// the failure is the code-review trigger.
94    ///
95    /// Order matters: tests assert this list matches [`ALL_COMPONENTS`] in
96    /// both content AND order, so a refactor that reshuffles the constant
97    /// surfaces immediately.
98    const PINNED_SNAPSHOT: &[(SchemaComponent, &str)] = &[
99        (SchemaComponent::DoctorReliability, "1.0.0"),
100        (SchemaComponent::Status, "1.0.0"),
101        (SchemaComponent::RepoUpdaterContract, "1.0.0"),
102        (SchemaComponent::ProcessTriageContract, "1.0.0"),
103    ];
104
105    #[test]
106    fn test_schema_versions_match_snapshot() {
107        assert_eq!(
108            ALL_COMPONENTS.len(),
109            PINNED_SNAPSHOT.len(),
110            "Number of components changed without snapshot update. \
111             Either add the missing entry to PINNED_SNAPSHOT or remove the unused entry from ALL_COMPONENTS."
112        );
113        for (i, ((c1, v1), (c2, v2))) in ALL_COMPONENTS
114            .iter()
115            .zip(PINNED_SNAPSHOT.iter())
116            .enumerate()
117        {
118            assert_eq!(
119                (c1, *v1),
120                (c2, *v2),
121                "Mismatch at position {i}: ALL_COMPONENTS has ({c1:?}, {v1}); \
122                 PINNED_SNAPSHOT has ({c2:?}, {v2}). \
123                 Bump procedure: edit current_version() AND PINNED_SNAPSHOT in the same commit."
124            );
125        }
126    }
127
128    #[test]
129    fn test_current_version_is_const_eval_friendly() {
130        // Call current_version() in a const-eval context. The compiler will
131        // refuse if it ever stops being a `const fn`.
132        const _DR: &str = current_version(SchemaComponent::DoctorReliability);
133        const _ST: &str = current_version(SchemaComponent::Status);
134        const _RU: &str = current_version(SchemaComponent::RepoUpdaterContract);
135        const _PT: &str = current_version(SchemaComponent::ProcessTriageContract);
136    }
137
138    #[test]
139    fn test_versions_match_semver_shape() {
140        for &(_, version) in ALL_COMPONENTS {
141            let parts: Vec<&str> = version.split('.').collect();
142            assert_eq!(
143                parts.len(),
144                3,
145                "version {version} must be MAJOR.MINOR.PATCH (got {} parts)",
146                parts.len()
147            );
148            for p in parts {
149                assert!(
150                    p.chars().all(|ch| ch.is_ascii_digit()),
151                    "version {version} contains non-numeric component {p}"
152                );
153            }
154        }
155    }
156
157    #[test]
158    fn test_all_components_unique() {
159        use std::collections::HashSet;
160        let set: HashSet<_> = ALL_COMPONENTS.iter().map(|(c, _)| *c).collect();
161        assert_eq!(
162            set.len(),
163            ALL_COMPONENTS.len(),
164            "duplicate SchemaComponent in ALL_COMPONENTS"
165        );
166    }
167}