polyc-runtime 2026.9.0

Shared Unix-coherence runtime for polychrome binaries: logging, health/metrics side-server, signals.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
//! Update compatibility classifier — the safety interlock at the heart of the
//! update model.
//!
//! Pure and free of I/O: given the running build's [`Fingerprint`] and an
//! available release's fingerprint, [`Fingerprint::classify`] sorts the change
//! into one of three tiers, and [`StagedBundle::evaluate`] enforces the
//! refuse-incompatible interlock for config-as-data bundles.
//!
//! # The fingerprint
//!
//! A [`Fingerprint`] captures only what decides whether one build can talk to
//! another and read another's data — four axes:
//!
//! - the Protobuf/wire descriptor version (`wire_version`),
//! - the event-log schema version (`eventlog_schema`),
//! - the CRD `apiVersion` the controller reconciles (`crd_api_version`),
//! - a hash over the tool catalog (`tool_catalog_hash`) — the stand-in for the
//!   whole config-as-data surface (prompts, persona defs, model routing).
//!
//! The first three are *format* axes: a difference in any of them means two
//! builds cannot interoperate without a coordinated move. The fourth is the
//! *config-as-data* axis: it moves without a binary change at all.
//!
//! # Classification
//!
//! Comparing the running fingerprint against an available one:
//!
//! - [`Compatibility::Cold`] — a format axis moved (wire, event log, or CRD).
//!   Peers cannot interoperate across the change; it needs a coordinated
//!   redeploy of the whole fleet.
//! - [`Compatibility::Hot`] — the format axes match and only the config-as-data
//!   surface moved. The running binary reloads the new catalog; no restart.
//! - [`Compatibility::Warm`] — the fingerprint is otherwise identical, so the
//!   only thing that moved is binary internals. A wire- and schema-compatible
//!   binary swap, picked up on restart.
//!
//! Binary releases and config-as-data bundles are separate delivery channels
//! (the Expo split of native app-store builds versus over-the-air JS bundles):
//! a warm/cold binary roll never simultaneously bumps the catalog hash, and a
//! hot config push never bumps a format axis. The classifier reads the
//! resulting fingerprint delta, so each artifact lands in exactly one tier.
//!
//! # The interlock
//!
//! A [`StagedBundle`] is a config-as-data payload plus the [`RuntimeTarget`] it
//! was authored against — the analog of an Expo update's `runtimeVersion`. The
//! running binary applies the bundle only if its own runtime matches the target
//! exactly; otherwise [`StagedBundle::evaluate`] returns
//! [`Compatibility::Incompatible`] and the bundle is refused rather than applied
//! against a runtime it was never built for.

/// The compatibility fingerprint of a build: the four axes that decide whether
/// two builds interoperate and can read one another's data.
///
/// Only equality matters — the numeric versions are compared for identity, not
/// ordering, because any difference on a format axis is a coordinated move
/// regardless of direction.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Fingerprint {
    /// Protobuf/wire descriptor version the build speaks.
    pub wire_version: u32,
    /// Event-log schema version the build reads and writes.
    pub eventlog_schema: u32,
    /// CRD `apiVersion` the controller reconciles (e.g. `polychrome.dev/v1`).
    pub crd_api_version: String,
    /// Hash over the tool catalog — the config-as-data surface (prompts,
    /// persona defs, model routing) that moves without a binary change.
    pub tool_catalog_hash: String,
}

impl Fingerprint {
    /// Build a fingerprint from its four axes.
    #[must_use]
    pub fn new(
        wire_version: u32,
        eventlog_schema: u32,
        crd_api_version: impl Into<String>,
        tool_catalog_hash: impl Into<String>,
    ) -> Self {
        Self {
            wire_version,
            eventlog_schema,
            crd_api_version: crd_api_version.into(),
            tool_catalog_hash: tool_catalog_hash.into(),
        }
    }

    /// The format-axis subset of this fingerprint — the runtime a config-as-data
    /// bundle must pin to ride on top of this build.
    #[must_use]
    pub fn runtime_target(&self) -> RuntimeTarget {
        RuntimeTarget {
            wire_version: self.wire_version,
            eventlog_schema: self.eventlog_schema,
            crd_api_version: self.crd_api_version.clone(),
        }
    }

    /// Classify `available` relative to this (the running) build.
    ///
    /// The result names the least-disruptive move that applies the available
    /// release: [`Compatibility::Cold`] when a format axis moved,
    /// [`Compatibility::Hot`] when only the config-as-data surface moved, and
    /// [`Compatibility::Warm`] when the fingerprint is otherwise identical (a
    /// wire- and schema-compatible binary swap). This path never refuses — a
    /// full release brings its own binary, so a format change is a coordinated
    /// redeploy rather than an incompatibility. See [`StagedBundle::evaluate`]
    /// for the refuse-incompatible interlock on config-as-data bundles.
    #[must_use]
    pub fn classify(&self, available: &Self) -> Compatibility {
        // Format gate: any wire / event-log / CRD move is a coordinated redeploy
        // and dominates a config-as-data move layered on top of it.
        if self.wire_version != available.wire_version
            || self.eventlog_schema != available.eventlog_schema
            || self.crd_api_version != available.crd_api_version
        {
            return Compatibility::Cold;
        }
        // Formats interoperate. A config-as-data move reloads hot.
        if self.tool_catalog_hash != available.tool_catalog_hash {
            return Compatibility::Hot;
        }
        // Fingerprint-identical: whatever moved is confined to binary internals,
        // a wire- and schema-compatible swap picked up on restart.
        Compatibility::Warm
    }
}

/// The format-axis subset of a [`Fingerprint`].
///
/// These are the wire, event-log, and CRD versions a staged config-as-data
/// bundle is authored against — the exact-match "runtime version" a bundle
/// pins, the analog of an Expo update's `runtimeVersion`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimeTarget {
    /// Protobuf/wire descriptor version the bundle was authored against.
    pub wire_version: u32,
    /// Event-log schema version the bundle was authored against.
    pub eventlog_schema: u32,
    /// CRD `apiVersion` the bundle was authored against.
    pub crd_api_version: String,
}

impl RuntimeTarget {
    /// Build a runtime target from its three format axes.
    #[must_use]
    pub fn new(
        wire_version: u32,
        eventlog_schema: u32,
        crd_api_version: impl Into<String>,
    ) -> Self {
        Self {
            wire_version,
            eventlog_schema,
            crd_api_version: crd_api_version.into(),
        }
    }

    /// The first axis on which `running` fails to satisfy this target, if any.
    ///
    /// Axes are checked in wire → event-log → CRD order; the first mismatch is
    /// reported, so a single [`Incompatibility`] names the reason to refuse.
    #[must_use]
    fn mismatch(&self, running: &Fingerprint) -> Option<Incompatibility> {
        if self.wire_version != running.wire_version {
            Some(Incompatibility::Wire)
        } else if self.eventlog_schema != running.eventlog_schema {
            Some(Incompatibility::EventLog)
        } else if self.crd_api_version != running.crd_api_version {
            Some(Incompatibility::Crd)
        } else {
            None
        }
    }
}

/// A staged config-as-data bundle: a hot payload (new tool catalog, and with it
/// prompts / persona defs / model routing) plus the [`RuntimeTarget`] it was
/// authored against.
///
/// The bundle rides on top of whatever binary is already running, so it declares
/// the runtime it needs and the running binary refuses it unless its own runtime
/// matches exactly — the refuse-incompatible interlock.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StagedBundle {
    /// The runtime this bundle was authored against; the running binary must
    /// match it exactly to apply the bundle.
    pub target: RuntimeTarget,
    /// The config-as-data payload identity this bundle delivers (the new tool
    /// catalog hash).
    pub catalog_hash: String,
}

impl StagedBundle {
    /// Build a staged bundle from its target runtime and payload hash.
    #[must_use]
    pub fn new(target: RuntimeTarget, catalog_hash: impl Into<String>) -> Self {
        Self {
            target,
            catalog_hash: catalog_hash.into(),
        }
    }

    /// Decide whether `running` may apply this bundle.
    ///
    /// Returns [`Compatibility::Hot`] when the running runtime matches the
    /// bundle's target exactly (the bundle reloads with no restart), or
    /// [`Compatibility::Incompatible`] naming the first mismatching axis when it
    /// does not — the bundle is refused rather than applied against a runtime it
    /// was never built for.
    #[must_use]
    pub fn evaluate(&self, running: &Fingerprint) -> Compatibility {
        self.target
            .mismatch(running)
            .map_or(Compatibility::Hot, Compatibility::Incompatible)
    }
}

/// How an available release or staged bundle relates to the running build.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Compatibility {
    /// Config-as-data only: reload the new catalog, no restart.
    Hot,
    /// Wire- and schema-compatible binary change: restart to apply.
    Warm,
    /// A wire / event-log / CRD format change: coordinated fleet redeploy.
    Cold,
    /// The running runtime cannot satisfy a staged bundle's target — refused,
    /// with the axis that failed the exact-match check.
    Incompatible(Incompatibility),
}

/// The format axis on which a running runtime fails to satisfy a staged bundle's
/// target.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Incompatibility {
    /// The Protobuf/wire descriptor version differs.
    Wire,
    /// The event-log schema version differs.
    EventLog,
    /// The reconciled CRD `apiVersion` differs.
    Crd,
}

impl std::fmt::Display for Incompatibility {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let axis = match self {
            Self::Wire => "the wire protocol version",
            Self::EventLog => "the event-log schema version",
            Self::Crd => "the reconciled CRD apiVersion",
        };
        write!(f, "{axis} differs from the target the bundle was built for")
    }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::pedantic, clippy::nursery, missing_docs)]

    use super::*;

    /// Shorthand for a fingerprint over the four axes.
    fn fp(wire: u32, schema: u32, crd: &str, catalog: &str) -> Fingerprint {
        Fingerprint::new(wire, schema, crd, catalog)
    }

    // --- Table-driven classification: (running × available) → expected tier ---

    #[test]
    fn classify_covers_hot_warm_cold() {
        let crd = "polychrome.dev/v1";
        let cases: &[(&str, Fingerprint, Fingerprint, Compatibility)] = &[
            (
                "identical fingerprint → warm (binary-internal change on restart)",
                fp(3, 7, crd, "catalog-a"),
                fp(3, 7, crd, "catalog-a"),
                Compatibility::Warm,
            ),
            (
                "only the tool catalog moved → hot (config-as-data reload)",
                fp(3, 7, crd, "catalog-a"),
                fp(3, 7, crd, "catalog-b"),
                Compatibility::Hot,
            ),
            (
                "wire version moved → cold",
                fp(3, 7, crd, "catalog-a"),
                fp(4, 7, crd, "catalog-a"),
                Compatibility::Cold,
            ),
            (
                "event-log schema moved → cold",
                fp(3, 7, crd, "catalog-a"),
                fp(3, 8, crd, "catalog-a"),
                Compatibility::Cold,
            ),
            (
                "CRD apiVersion moved → cold",
                fp(3, 7, crd, "catalog-a"),
                fp(3, 7, "polychrome.dev/v2", "catalog-a"),
                Compatibility::Cold,
            ),
            (
                "format axis moved AND catalog moved → cold dominates hot",
                fp(3, 7, crd, "catalog-a"),
                fp(4, 7, crd, "catalog-b"),
                Compatibility::Cold,
            ),
        ];

        for (name, running, available, expected) in cases {
            assert_eq!(
                running.classify(available),
                *expected,
                "classify case: {name}"
            );
        }
    }

    #[test]
    fn classify_is_direction_agnostic_on_format_axes() {
        // A format move is a coordinated redeploy whether the running build is
        // ahead of or behind the available one.
        let old = fp(3, 7, "polychrome.dev/v1", "c");
        let new = fp(4, 8, "polychrome.dev/v2", "c");
        assert_eq!(old.classify(&new), Compatibility::Cold);
        assert_eq!(new.classify(&old), Compatibility::Cold);
    }

    // --- Refuse-incompatible interlock on staged config-as-data bundles ---

    #[test]
    fn bundle_applies_hot_when_runtime_matches_target() {
        let running = fp(3, 7, "polychrome.dev/v1", "catalog-a");
        // Authored against the running runtime, delivering a new catalog.
        let bundle = StagedBundle::new(running.runtime_target(), "catalog-b");
        assert_eq!(bundle.evaluate(&running), Compatibility::Hot);
    }

    #[test]
    fn bundle_applies_hot_even_when_payload_matches_current_catalog() {
        // A no-op payload is still a hot-tier application, not a refusal: the
        // interlock only gates on the runtime target, never the payload.
        let running = fp(3, 7, "polychrome.dev/v1", "catalog-a");
        let bundle = StagedBundle::new(running.runtime_target(), "catalog-a");
        assert_eq!(bundle.evaluate(&running), Compatibility::Hot);
    }

    #[test]
    fn bundle_refused_when_running_cannot_satisfy_target() {
        let running = fp(3, 7, "polychrome.dev/v1", "catalog-a");
        let cases: &[(&str, RuntimeTarget, Incompatibility)] = &[
            (
                "bundle built for a newer wire version",
                RuntimeTarget::new(4, 7, "polychrome.dev/v1"),
                Incompatibility::Wire,
            ),
            (
                "bundle built for a newer event-log schema",
                RuntimeTarget::new(3, 8, "polychrome.dev/v1"),
                Incompatibility::EventLog,
            ),
            (
                "bundle built for a different CRD apiVersion",
                RuntimeTarget::new(3, 7, "polychrome.dev/v2"),
                Incompatibility::Crd,
            ),
        ];

        for (name, target, reason) in cases {
            let bundle = StagedBundle::new(target.clone(), "catalog-b");
            assert_eq!(
                bundle.evaluate(&running),
                Compatibility::Incompatible(*reason),
                "interlock case: {name}"
            );
        }
    }

    #[test]
    fn interlock_reports_wire_before_other_axes() {
        // When several axes are unsatisfiable at once, the first (wire) is named.
        let running = fp(3, 7, "polychrome.dev/v1", "catalog-a");
        let bundle = StagedBundle::new(RuntimeTarget::new(9, 9, "polychrome.dev/v9"), "catalog-b");
        assert_eq!(
            bundle.evaluate(&running),
            Compatibility::Incompatible(Incompatibility::Wire),
        );
    }

    #[test]
    fn incompatibility_display_names_the_axis() {
        assert_eq!(
            Incompatibility::Wire.to_string(),
            "the wire protocol version differs from the target the bundle was built for",
        );
        assert_eq!(
            Incompatibility::EventLog.to_string(),
            "the event-log schema version differs from the target the bundle was built for",
        );
        assert_eq!(
            Incompatibility::Crd.to_string(),
            "the reconciled CRD apiVersion differs from the target the bundle was built for",
        );
    }
}