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}