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    /// Incident event schema (shared remediation reason-code ledger).
50    IncidentLedger,
51    /// Worker capability fact schema (exact host/user/path inventory).
52    WorkerFacts,
53    /// Durable proof-intent record schema (deferred proof queue).
54    ProofIntent,
55    /// Durable proof-replay state record schema (deferred proof conveyor).
56    ProofReplayState,
57    /// Worker temporary-bypass record schema (transient-quarantine lifecycle).
58    WorkerBypassRecord,
59    /// Shared secret-redaction / privacy policy for all remediation data.
60    RedactionPolicy,
61    /// Operator-facing remediation view rendered by the TUI/web dashboards.
62    RemediationView,
63    /// Canonical placement / visibility / strict-remote control plan
64    /// (`rch diagnose` placement surface + `rch exec` control state).
65    PlacementPlan,
66    /// Fleet binary deploy audit record: release provenance, verification
67    /// status, and rollback trail (`rch fleet deploy` audit log).
68    FleetDeployAudit,
69}
70
71/// The canonical version string for a given component.
72///
73/// `const fn` so callers can use the result in `pub const` declarations
74/// (preserves the existing call sites that read into compile-time
75/// constants).
76#[must_use]
77pub const fn current_version(component: SchemaComponent) -> &'static str {
78    match component {
79        SchemaComponent::DoctorReliability => "1.0.0",
80        SchemaComponent::Status => "1.0.0",
81        SchemaComponent::RepoUpdaterContract => "1.0.0",
82        SchemaComponent::ProcessTriageContract => "1.0.0",
83        SchemaComponent::IncidentLedger => "1.0.0",
84        SchemaComponent::WorkerFacts => "1.0.0",
85        SchemaComponent::ProofIntent => "1.0.0",
86        SchemaComponent::ProofReplayState => "1.0.0",
87        SchemaComponent::WorkerBypassRecord => "1.0.0",
88        SchemaComponent::RedactionPolicy => "1.0.0",
89        SchemaComponent::RemediationView => "1.0.0",
90        SchemaComponent::PlacementPlan => "1.0.0",
91        SchemaComponent::FleetDeployAudit => "1.0.0",
92    }
93}
94
95/// Every component, paired with its current version. Iterable in tests
96/// and at runtime.
97pub const ALL_COMPONENTS: &[(SchemaComponent, &str)] = &[
98    (
99        SchemaComponent::DoctorReliability,
100        current_version(SchemaComponent::DoctorReliability),
101    ),
102    (
103        SchemaComponent::Status,
104        current_version(SchemaComponent::Status),
105    ),
106    (
107        SchemaComponent::RepoUpdaterContract,
108        current_version(SchemaComponent::RepoUpdaterContract),
109    ),
110    (
111        SchemaComponent::ProcessTriageContract,
112        current_version(SchemaComponent::ProcessTriageContract),
113    ),
114    (
115        SchemaComponent::IncidentLedger,
116        current_version(SchemaComponent::IncidentLedger),
117    ),
118    (
119        SchemaComponent::WorkerFacts,
120        current_version(SchemaComponent::WorkerFacts),
121    ),
122    (
123        SchemaComponent::ProofIntent,
124        current_version(SchemaComponent::ProofIntent),
125    ),
126    (
127        SchemaComponent::ProofReplayState,
128        current_version(SchemaComponent::ProofReplayState),
129    ),
130    (
131        SchemaComponent::WorkerBypassRecord,
132        current_version(SchemaComponent::WorkerBypassRecord),
133    ),
134    (
135        SchemaComponent::RedactionPolicy,
136        current_version(SchemaComponent::RedactionPolicy),
137    ),
138    (
139        SchemaComponent::RemediationView,
140        current_version(SchemaComponent::RemediationView),
141    ),
142    (
143        SchemaComponent::PlacementPlan,
144        current_version(SchemaComponent::PlacementPlan),
145    ),
146    (
147        SchemaComponent::FleetDeployAudit,
148        current_version(SchemaComponent::FleetDeployAudit),
149    ),
150];
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    /// Pinned snapshot of every component's version. Bumping a version
157    /// in [`current_version`] without updating this table fails the test —
158    /// the failure is the code-review trigger.
159    ///
160    /// Order matters: tests assert this list matches [`ALL_COMPONENTS`] in
161    /// both content AND order, so a refactor that reshuffles the constant
162    /// surfaces immediately.
163    const PINNED_SNAPSHOT: &[(SchemaComponent, &str)] = &[
164        (SchemaComponent::DoctorReliability, "1.0.0"),
165        (SchemaComponent::Status, "1.0.0"),
166        (SchemaComponent::RepoUpdaterContract, "1.0.0"),
167        (SchemaComponent::ProcessTriageContract, "1.0.0"),
168        (SchemaComponent::IncidentLedger, "1.0.0"),
169        (SchemaComponent::WorkerFacts, "1.0.0"),
170        (SchemaComponent::ProofIntent, "1.0.0"),
171        (SchemaComponent::ProofReplayState, "1.0.0"),
172        (SchemaComponent::WorkerBypassRecord, "1.0.0"),
173        (SchemaComponent::RedactionPolicy, "1.0.0"),
174        (SchemaComponent::RemediationView, "1.0.0"),
175        (SchemaComponent::PlacementPlan, "1.0.0"),
176        (SchemaComponent::FleetDeployAudit, "1.0.0"),
177    ];
178
179    #[test]
180    fn test_schema_versions_match_snapshot() {
181        assert_eq!(
182            ALL_COMPONENTS.len(),
183            PINNED_SNAPSHOT.len(),
184            "Number of components changed without snapshot update. \
185             Either add the missing entry to PINNED_SNAPSHOT or remove the unused entry from ALL_COMPONENTS."
186        );
187        for (i, ((c1, v1), (c2, v2))) in ALL_COMPONENTS
188            .iter()
189            .zip(PINNED_SNAPSHOT.iter())
190            .enumerate()
191        {
192            assert_eq!(
193                (c1, *v1),
194                (c2, *v2),
195                "Mismatch at position {i}: ALL_COMPONENTS has ({c1:?}, {v1}); \
196                 PINNED_SNAPSHOT has ({c2:?}, {v2}). \
197                 Bump procedure: edit current_version() AND PINNED_SNAPSHOT in the same commit."
198            );
199        }
200    }
201
202    #[test]
203    fn test_current_version_is_const_eval_friendly() {
204        // Call current_version() in a const-eval context. The compiler will
205        // refuse if it ever stops being a `const fn`.
206        const _DR: &str = current_version(SchemaComponent::DoctorReliability);
207        const _ST: &str = current_version(SchemaComponent::Status);
208        const _RU: &str = current_version(SchemaComponent::RepoUpdaterContract);
209        const _PT: &str = current_version(SchemaComponent::ProcessTriageContract);
210    }
211
212    #[test]
213    fn test_versions_match_semver_shape() {
214        for &(_, version) in ALL_COMPONENTS {
215            let parts: Vec<&str> = version.split('.').collect();
216            assert_eq!(
217                parts.len(),
218                3,
219                "version {version} must be MAJOR.MINOR.PATCH (got {} parts)",
220                parts.len()
221            );
222            for p in parts {
223                assert!(
224                    p.chars().all(|ch| ch.is_ascii_digit()),
225                    "version {version} contains non-numeric component {p}"
226                );
227            }
228        }
229    }
230
231    #[test]
232    fn test_all_components_unique() {
233        use std::collections::HashSet;
234        let set: HashSet<_> = ALL_COMPONENTS.iter().map(|(c, _)| *c).collect();
235        assert_eq!(
236            set.len(),
237            ALL_COMPONENTS.len(),
238            "duplicate SchemaComponent in ALL_COMPONENTS"
239        );
240    }
241}