Skip to main content

yah_mesofact_bundle/
contract.rs

1//! The **bundle↔runtime contract** (R746-F6, W272 §7): a version for the
2//! interface between a bundle and whatever binary serves it.
3//!
4//! Before this, the only version in the format was
5//! [`SCHEMA_VERSION`](crate::SCHEMA_VERSION) — the wire shape of
6//! `manifest.toml`. That is a fraction of the real interface. A runtime also
7//! has to know where in the tree to find routes and assets, how it will be
8//! forked, and what `bins/` means. None of that was stated anywhere, which is
9//! why `runtime = "mesofact/0.8.18"` could only ever be an exact pin: a range
10//! like `mesofact ^0.8` asserts that every 0.8.x can serve this bundle, and
11//! nothing existed for that assertion to be *about*.
12//!
13//! So: a runtime **advertises** the contract versions it implements (recorded
14//! as `contract` on its published `RuntimeAssetManifest`, under the `store`
15//! feature), a bundle **requires** one
16//! ([`BundleManifest::requires_contract`](crate::BundleManifest::requires_contract)), and
17//! resolution refuses the pair when the advertised set doesn't contain the
18//! required version. Product semver (`mesofact 0.8.20`) stays a human-facing
19//! hint layered on top rather than the thing resolution trusts.
20//!
21//! # Contract version 1
22//!
23//! Four clauses. A change to **any** of them that an existing runtime would
24//! get wrong is a new contract version; a change no existing runtime can
25//! observe is not.
26//!
27//! 1. **Manifest schema.** `manifest.toml` at the bundle root, parsing as
28//!    [`BundleManifest`](crate::BundleManifest) at
29//!    [`SCHEMA_VERSION`](crate::SCHEMA_VERSION) `= 1`: `schema_version`,
30//!    `name`, `runtime`, `requires_contract`, and a `[content]` table of
31//!    bundle-relative path → BLAKE3.
32//! 2. **App tree layout.** Everything the runtime reads lives under `app/`:
33//!    `app/mesofact.routes.ts` (routes + config), `app/mesofact.config.toml`
34//!    (optional `[publish]` block for a `--revalidate` receiver),
35//!    `app/dist/**` (built TS + rendered assets), and each route's declared
36//!    `data_inputs` staged at `app/<rel>` — resolved against `<bundle>/app`
37//!    as the workload root, not against the machine that built it.
38//! 3. **Entrypoint calling convention.** The resolved binary is forked as
39//!    `<bin> serve --bundle <bundle-dir> --listen <addr>`. `serve` is a
40//!    *subcommand*: the published asset is the whole mesofact binary, and one
41//!    that takes `--bundle` as `argv[1]` exits on an unknown argument before
42//!    it ever binds.
43//! 4. **`bins/` and sidecars.** In a self-contained bundle the runtime is
44//!    `bins/<triple>/serve`; sidecars are `bins/<triple>/<name>` and `serve`
45//!    is reserved. A vanilla bundle carries no `bins/` at all and resolves
46//!    both from the node runtime-asset tier instead.
47//!
48//! # Bumping it
49//!
50//! Add the new version to [`IMPLEMENTED_CONTRACTS`], move
51//! [`BUNDLE_CONTRACT_VERSION`] to it, and add a row to the compatibility
52//! matrix (`tests/contract_matrix.rs`). The matrix has a gate test that fails
53//! when a known version has no row — a contract version nothing exercises is
54//! a comment, and the matrix is also what makes it safe to *widen* a bundle's
55//! accepted set later, because it is the evidence.
56//!
57//! Dropping a version from [`IMPLEMENTED_CONTRACTS`] is the breaking move: it
58//! strands every published bundle requiring it, and those are immutable.
59
60/// A contract version. Small dense integers, not semver — the whole point is
61/// that this is the thing semver ranges would have had to range over.
62pub type ContractVersion = u32;
63
64/// The contract version this build **emits** on every bundle it assembles.
65pub const BUNDLE_CONTRACT_VERSION: ContractVersion = 1;
66
67/// Every contract version a runtime built from this tree **implements**, and
68/// therefore what [`publish_runtime_asset`](crate::publish_runtime_asset)
69/// advertises by default.
70///
71/// A superset of `[BUNDLE_CONTRACT_VERSION]` in general: a runtime keeps
72/// serving older bundles long after the assembler stops emitting them, because
73/// published bundles are immutable and a node may still be asked to materialize
74/// one from years ago.
75pub const IMPLEMENTED_CONTRACTS: &[ContractVersion] = &[BUNDLE_CONTRACT_VERSION];
76
77/// What a resolution site demands of the runtime it is about to fork.
78///
79/// An enum rather than an `Option<ContractVersion>` on purpose: `None` at a
80/// call site reads as "nothing to pass" and gets copy-pasted into places that
81/// did have something to pass. [`Unchecked`](Self::Unchecked) has to be
82/// spelled, and spelling it invites the question of why.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum ContractRequirement {
85    /// The version the bundle's manifest requires. The resolved runtime must
86    /// advertise it.
87    Version(ContractVersion),
88
89    /// Resolve without a contract check. The sidecar case: `almanac-feed` is
90    /// versioned with yubaba and its interface is its own CLI, not the
91    /// bundle↔runtime contract, so holding it to *this* contract's versions
92    /// would be a category error. Nothing that resolves a **serve** runtime
93    /// may use this.
94    Unchecked,
95}
96
97impl ContractRequirement {
98    /// Whether `advertised` satisfies this requirement.
99    pub fn satisfied_by(&self, advertised: &[ContractVersion]) -> bool {
100        match self {
101            ContractRequirement::Unchecked => true,
102            ContractRequirement::Version(v) => advertised.contains(v),
103        }
104    }
105}
106
107/// Render an advertised set for an error message: `1` / `1, 2` / `(none)`.
108///
109/// `(none)` is reachable — a publisher can record an empty set — and reads
110/// far better in a refusal than `[]`.
111pub fn describe_contracts(advertised: &[ContractVersion]) -> String {
112    if advertised.is_empty() {
113        return "(none)".to_string();
114    }
115    advertised
116        .iter()
117        .map(|v| v.to_string())
118        .collect::<Vec<_>>()
119        .join(", ")
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    /// The emitted version must be one this tree's runtimes can actually
127    /// serve, or every bundle assembled here is born unservable.
128    #[test]
129    fn this_build_implements_what_it_emits() {
130        assert!(IMPLEMENTED_CONTRACTS.contains(&BUNDLE_CONTRACT_VERSION));
131    }
132
133    #[test]
134    fn satisfaction_is_set_membership() {
135        assert!(ContractRequirement::Version(1).satisfied_by(&[1]));
136        assert!(ContractRequirement::Version(1).satisfied_by(&[1, 2]));
137        assert!(!ContractRequirement::Version(2).satisfied_by(&[1]));
138        assert!(!ContractRequirement::Version(1).satisfied_by(&[]));
139        // Unchecked resolves against anything, including nothing.
140        assert!(ContractRequirement::Unchecked.satisfied_by(&[]));
141    }
142
143    #[test]
144    fn describe_reads_as_a_list() {
145        assert_eq!(describe_contracts(&[]), "(none)");
146        assert_eq!(describe_contracts(&[1]), "1");
147        assert_eq!(describe_contracts(&[1, 2, 3]), "1, 2, 3");
148    }
149}