caixa_core/version.rs
1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6/// A caixa's pinned version — a thin typed wrapper over a String that parses
7/// as [`semver::Version`] on demand.
8///
9/// Stored as a String at rest so authoring a `caixa.lisp` stays a single
10/// quoted literal. The typed form is reached through [`Self::parse`].
11#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
12#[serde(transparent)]
13pub struct CaixaVersion(pub String);
14
15impl CaixaVersion {
16 /// Parse and validate the wrapped string as semver.
17 pub fn parse(&self) -> Result<semver::Version, VersionError> {
18 semver::Version::parse(&self.0)
19 .map_err(|e| VersionError::semver(self.0.clone(), e.to_string()))
20 }
21
22 /// Borrow the string form.
23 #[must_use]
24 pub const fn as_str(&self) -> &str {
25 self.0.as_str()
26 }
27}
28
29impl fmt::Display for CaixaVersion {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 f.write_str(&self.0)
32 }
33}
34
35impl From<String> for CaixaVersion {
36 fn from(s: String) -> Self {
37 Self(s)
38 }
39}
40
41impl From<&str> for CaixaVersion {
42 fn from(s: &str) -> Self {
43 Self(s.to_string())
44 }
45}
46
47/// Substrate-canonical [`AsRef<str>`] projection on the [`CaixaVersion`]
48/// typed newtype — routes through the same [`CaixaVersion::as_str`]
49/// `pub const fn` scalar accessor the sibling [`fmt::Display`] impl
50/// and every downstream `&str`-shaped consumer already keys off, so
51/// any future consumer that binds a [`CaixaVersion`] through the
52/// standard-library `impl AsRef<str>` bound (a `Path`-shaped file-
53/// system reader on the operator side that accepts the version body
54/// as one segment of a per-caixa `versao/<v>/...` on-disk cache path,
55/// a builder-shaped API on the future `feira publish` writer verb
56/// that composes `<prefix><versao>` through a git-tag builder crate's
57/// `impl AsRef<str>` join step, a `HashMap<CaixaVersion, _>` lookup
58/// through the `map.get::<str>(v.as_ref())` shape a future
59/// version-keyed dispatch table lands on) reaches the wrapped
60/// [`String`] through one substrate-primitive dispatch rather than
61/// through the pre-lift `.as_str()` open-coded projection at every
62/// wire-up.
63///
64/// Peer of the sibling [`fmt::Display`] impl on the same primitive —
65/// both delegate to the shared [`CaixaVersion::as_str`] `pub const
66/// fn` accessor, so [`format!("{v}")`], `v.as_str()`, and
67/// `<CaixaVersion as AsRef<str>>::as_ref(&v)` resolve to the same
68/// byte-string per instance by construction. A future rebrand of the
69/// wrapped storage (a hypothetical widening to a typed [`semver::Version`]
70/// slot the roadmap acknowledges once eager parse-on-construct
71/// discipline lands, an internal normalization step that trims
72/// leading zeroes off pre-release identifiers, a per-cluster overlay
73/// the operator pins through a future `:versao-overrides` slot) that
74/// changes what [`CaixaVersion::as_str`] returns migrates every
75/// consumer of every one of the three paths in lockstep.
76///
77/// Same "route the trait impl through the substrate-primitive
78/// accessor" discipline the sibling [`fmt::Display`] impl on this
79/// type already carries — extends it onto the standard-library
80/// [`AsRef<str>`] projection axis every third-party API that takes
81/// `impl AsRef<str>` (the [`std::path::Path::new`] / [`std::fs`]
82/// interop surface, [`std::process::Command::arg`], the peer
83/// `tracing::field::Value` recorder's `Str`-arm, every `clap`-side
84/// `value_parser!` fold that accepts an owned newtype through
85/// `impl AsRef<str>`) already binds through. Rust-side newtype
86/// convention pairs `AsRef<str>` and [`fmt::Display`] on the same
87/// primitive so a caller who has one has both; before this lift,
88/// [`CaixaVersion`] carried [`fmt::Display`] but not the paired
89/// [`AsRef<str>`] impl the convention names.
90///
91/// The first standard-library trait added to [`CaixaVersion`] beyond
92/// the pre-existing [`serde::Serialize`] / [`serde::Deserialize`] /
93/// [`Debug`] / [`Clone`] / [`PartialEq`] / [`Eq`] / [`Hash`] derives
94/// and the paired [`fmt::Display`] / [`From<String>`] / [`From<&str>`]
95/// hand-written impls. Pinned load-bearing by
96/// [`tests::caixa_version_as_ref_str_routes_through_as_str_accessor`]
97/// (byte-parity pin against [`CaixaVersion::as_str`]) — any future
98/// silent detour that routes the impl through a divergent projection
99/// (a `Cow<'_, str>` intermediate, a stray `.to_lowercase()`
100/// normalization, a swap onto a per-arm inline `&self.0.as_str()`
101/// re-inlining) trips at caixa-core test time under `assert_eq!`
102/// rather than at a downstream `impl AsRef<str>`-bound consumer's
103/// silent split.
104impl AsRef<str> for CaixaVersion {
105 fn as_ref(&self) -> &str {
106 self.as_str()
107 }
108}
109
110/// Trait-idiomatic *owned-`String`* reverse projection on the
111/// [`CaixaVersion`] newtype primitive — the owned-heap-string inverse
112/// of the pre-existing [`From<String> for CaixaVersion`] /
113/// [`From<&str> for CaixaVersion`] forward-projection pair on this
114/// primitive. Returns the wrapped [`String`] verbatim ([`Self::0`],
115/// a move of the pre-existing heap allocation — no re-copy of the
116/// per-instance version body's bytes), so every consumer that binds a
117/// [`CaixaVersion`] through the standard-library `.into()` /
118/// [`From<Self> for String`] (equivalently [`Into<String>`]) axis
119/// reaches the wrapped byte-string through one substrate-primitive
120/// dispatch rather than through a `.as_str().to_owned()` /
121/// `.to_string()` allocating detour whose bounds have no compile-time
122/// link back to the newtype's storage.
123///
124/// A future consumer that wants to unwrap a [`CaixaVersion`] into an
125/// owned [`String`] — a `serde_json::Value::String(versao.into())`
126/// structured-payload composer where the `Value::String` arm typing
127/// demands an owned [`String`] and the sibling
128/// [`AsRef<str>`]-borrowed axis forces an explicit `.to_owned()`
129/// restatement at every call site, a future
130/// `HashMap::<String, _>::from_iter([(versao.into(), _)])` per-versao
131/// lookup where the map's key type is owned [`String`] rather than
132/// [`&str`] borrowed from a stashed [`CaixaVersion`], a future
133/// `Cow::<'static, str>::Owned(versao.into())` composer where the
134/// owned arm typing rules out the borrowed [`AsRef<str>`] return —
135/// reaches the wrapped [`String`] through this one dispatch, avoiding
136/// the pre-lift double-allocation (`.as_str().to_owned()` on the owned
137/// path would allocate a fresh [`String`] rather than reuse the
138/// wrapper's own heap allocation).
139///
140/// Opens the trait-idiomatic *owned-`String`* reverse-projection axis
141/// on the substrate's core String-wrapper newtype primitive
142/// [`CaixaVersion`], mirroring the paired owned-`String` forward-
143/// projection family the sibling closed-set fieldless typed enums
144/// ([`crate::supervisor::RestartStrategy`] (7baa18a, first-mover),
145/// [`crate::supervisor::RestartPolicy`] (7851725),
146/// [`crate::CaixaKind`] (per its own doc block, third peer), plus the
147/// remaining twelve closed-set enums) already carry — Rust's standard
148/// library does not derive `From<Self> for String` from `From<String>
149/// for Self`, so every newtype that carries a forward `From<String>`
150/// constructor but not the paired reverse-unwrap axis forces every
151/// call site through a `.to_string()` / `.as_str().to_owned()` detour
152/// that allocates fresh bytes rather than moving the wrapper's own
153/// heap allocation.
154///
155/// Preserves the two-path split on the wrapped byte-string: the paired
156/// [`AsRef<str>`] and [`fmt::Display`] impls stay reachable for the
157/// borrowed `&str` and formatter-output paths, this impl closes the
158/// owned-`String` reverse axis. Same "one dispatch on the substrate
159/// primitive" discipline the peer forward `From<String> for
160/// CaixaVersion` / `From<&str> for CaixaVersion` constructors carry,
161/// now extended onto the owned-heap-string reverse projection.
162///
163/// Pinned load-bearing by
164/// [`tests::caixa_version_from_into_owned_string_returns_wrapped_body`]
165/// (byte-parity pin against [`CaixaVersion::as_str`] on the same
166/// instance) and
167/// [`tests::caixa_version_from_into_owned_string_and_as_str_agree_on_every_shape`]
168/// (cross-axis partition pin against the paired borrowed
169/// [`AsRef<str>`] impl and the sibling [`fmt::Display`]-routed
170/// [`ToString::to_string`] surface, plus a round-trip witness through
171/// the paired forward [`From<String> for CaixaVersion`] constructor
172/// closing the two-way `Self → String → Self` cycle by construction).
173impl From<CaixaVersion> for String {
174 fn from(v: CaixaVersion) -> String {
175 v.0
176 }
177}
178
179/// Trait-idiomatic *borrowed-input, owned-`String` output* reverse
180/// projection on the [`CaixaVersion`] newtype primitive — the
181/// borrowed-input companion to the paired owned-input
182/// [`From<CaixaVersion> for String`] impl immediately above. Routes
183/// byte-for-byte through the substrate-primitive
184/// [`CaixaVersion::as_str`] `pub const fn` accessor (via
185/// [`str::to_owned`]) so every consumer that holds a
186/// borrowed [`&CaixaVersion`] and needs an owned [`String`] — a
187/// `[…].iter().map(String::from).collect::<Vec<_>>()` per-instance
188/// materializer over `&[CaixaVersion]` (whose iterator yields
189/// `&CaixaVersion`, not `CaixaVersion`, so the owned-input
190/// [`From<CaixaVersion> for String`] axis alone forces every call site
191/// through an explicit `.clone()` / dereference restatement), a future
192/// `HashMap::<String, _>::from_iter` that keys off a borrowed-
193/// iteration axis where cloning the wrapper would allocate one
194/// [`String`] beyond the map entry's own, a future
195/// `serde_json::Value::String(String::from(&caixa.versao))`
196/// structured-payload composer that owns the emit-path without moving
197/// out of a borrowed field — reaches the wrapped byte-string through
198/// this one dispatch on the substrate primitive.
199///
200/// Second corner on the `{Self, &Self} → String` reverse-projection
201/// family opened on the paired owned-input
202/// [`From<CaixaVersion> for String`] impl immediately above. Rust's
203/// `From` trait does not derive the `From<&Self>` sibling from a
204/// `From<Self>` impl (the blanket
205/// `impl<T, U> From<&T> for U where T: Clone, U: From<T>` does not
206/// exist in `core`), so every newtype that carries the owned-input
207/// reverse axis but not the borrowed-input axis forces every borrowed
208/// call site through a `.clone()` / `<String>::from(v.clone())` detour
209/// whose type bounds have no compile-time link back to the newtype.
210///
211/// Pinned load-bearing by
212/// [`tests::caixa_version_from_borrowed_into_owned_string_routes_through_as_str_accessor`]
213/// (byte-parity pin against [`CaixaVersion::as_str`] via a borrowed
214/// input) and
215/// [`tests::caixa_version_from_owned_and_borrowed_into_string_agree_on_every_shape`]
216/// (cross-axis partition pin against the paired owned-input
217/// [`From<CaixaVersion> for String`] impl on the same instance,
218/// closing the "owned-input move vs. borrowed-input clone" bifurcation
219/// on the same wrapped body).
220impl From<&CaixaVersion> for String {
221 fn from(v: &CaixaVersion) -> String {
222 v.as_str().to_owned()
223 }
224}
225
226/// Trait-idiomatic *owned-input, [`std::borrow::Cow<'static, str>`]
227/// output* reverse projection on the [`CaixaVersion`] newtype
228/// primitive — the [`Cow<'static, str>`] companion to the paired
229/// owned-input [`From<CaixaVersion> for String`] impl (999a310) on
230/// the same primitive. Routes through
231/// [`std::borrow::Cow::Owned`]`(v.0)`, moving the wrapper's own heap
232/// allocation through verbatim (no re-copy of the per-instance version
233/// body's bytes, no allocating detour through
234/// [`CaixaVersion::as_str`] + [`str::to_owned`]) — so every consumer
235/// that binds a [`CaixaVersion`] through the standard-library `.into()`
236/// / [`From<Self> for Cow<'static, str>`] axis reaches the wrapped
237/// byte-string through one substrate-primitive dispatch on the exact
238/// same heap allocation the manifest-parse forward
239/// [`From<String> for CaixaVersion`] constructor accepted.
240///
241/// A future consumer that wants a [`Cow<'static, str>`]-typed handle
242/// on a [`CaixaVersion`] — a
243/// `metric_label: Cow<'static, str> = versao.into()` structured-log
244/// key on a future per-caixa `caixa-operator` reconciliation counter
245/// (whose emit surface types metric keys as `Cow<'static, str>` so
246/// static compile-time literals and dynamic version bodies share the
247/// same key-slot without an unconditional heap allocation on the
248/// literal path), a future
249/// `HashMap::<Cow<'static, str>, _>::from_iter([(versao.into(), _)])`
250/// per-versao lookup where the map's key type is
251/// [`Cow<'static, str>`] rather than owned [`String`] so
252/// literal-lifetime keys can share the same map without wrapping in an
253/// extra [`String`] allocation, a future M4 admission-webhook
254/// rejection body whose per-arm error message composes through
255/// `format!("{}", Cow::<'static, str>::from(caixa.versao))` where the
256/// [`Cow<'static, str>`] intermediate is what the sibling error-frame
257/// composer accepts — reaches the wrapped byte-string through this
258/// one dispatch, without the pre-lift `.to_string().into()` /
259/// `Cow::Owned(String::from(v))` double-hop that would allocate a
260/// fresh intermediary [`String`] on the way to the same
261/// [`Cow::Owned`] arm.
262///
263/// Deliberately returns [`std::borrow::Cow::Owned`] rather than
264/// [`std::borrow::Cow::Borrowed`] — the substrate-primitive
265/// [`CaixaVersion::as_str`] accessor's return does not carry the
266/// `&'static str` lifetime by construction (the wrapped [`String`]
267/// storage is a runtime heap allocation, not a compile-time literal),
268/// so the [`Cow<'static, str>`] output shape rules out the borrowed
269/// arm and the owned arm is the type-correct projection. Peer of the
270/// paired owned-input [`From<CaixaVersion> for String`] impl on the
271/// same primitive — both route through the wrapper's own heap
272/// allocation via a move on `v.0`, preserving the zero-copy
273/// discipline the substrate opens on its String-wrapper newtype
274/// primitive.
275///
276/// Opens the trait-idiomatic *owned-input, [`Cow<'static, str>`]*
277/// reverse-projection axis on the substrate's core String-wrapper
278/// newtype primitive [`CaixaVersion`], mirroring the paired
279/// [`Cow<'static, str>`] *forward*-projection family the sibling
280/// closed-set fieldless typed enums (via
281/// [`crate::supervisor::RestartStrategy`],
282/// [`crate::supervisor::RestartPolicy`], and the remaining twelve
283/// closed-set enums) already carry — on the enum peers, the paired
284/// axis returns [`Cow::Borrowed`] because the accessor returns
285/// `&'static str`; on this newtype the paired axis returns
286/// [`Cow::Owned`] because the wrapped storage is runtime-allocated.
287/// Rust's standard library does not derive `From<Self> for
288/// Cow<'static, str>` from `From<Self> for String` (nor derive
289/// `From<&Self>` from `From<Self>`), so every newtype that carries a
290/// reverse `From<Self> for String` unwrap axis but not the paired
291/// [`Cow<'static, str>`] axis forces every
292/// [`Cow<'static, str>`]-typed call site through a `.to_string().into()`
293/// double-allocation detour that heap-allocates a fresh intermediary
294/// [`String`] between the wrapper and the [`Cow::Owned`] arm.
295///
296/// Pinned load-bearing by
297/// [`tests::caixa_version_from_into_owned_cow_str_returns_owned_wrapped_body`]
298/// (byte-parity + [`Cow::Owned`]-arm pin against
299/// [`CaixaVersion::as_str`] on the same instance, plus a round-trip
300/// witness through the paired [`From<String> for CaixaVersion`]
301/// constructor) and
302/// [`tests::caixa_version_from_into_owned_cow_str_and_string_agree_on_every_shape`]
303/// (cross-axis partition pin against the paired owned-input
304/// [`From<CaixaVersion> for String`] impl on the same instance,
305/// closing the "owned-input into [`String`] vs. owned-input into
306/// [`Cow<'static, str>`]" bifurcation on the same wrapped body).
307impl From<CaixaVersion> for std::borrow::Cow<'static, str> {
308 fn from(v: CaixaVersion) -> std::borrow::Cow<'static, str> {
309 std::borrow::Cow::Owned(v.0)
310 }
311}
312
313/// Trait-idiomatic *borrowed-input, [`std::borrow::Cow<'static, str>`]
314/// output* reverse projection on the [`CaixaVersion`] newtype
315/// primitive — the borrowed-input companion to the paired owned-input
316/// [`From<CaixaVersion> for std::borrow::Cow<'static, str>`] impl
317/// immediately above. Routes byte-for-byte through the
318/// substrate-primitive [`CaixaVersion::as_str`] `pub const fn`
319/// accessor (via [`str::to_owned`] wrapped in
320/// [`std::borrow::Cow::Owned`]) so every consumer that holds a
321/// borrowed [`&CaixaVersion`] and needs a [`Cow<'static, str>`] —
322/// a `[…].iter().map(Cow::<'static, str>::from).collect::<Vec<_>>()`
323/// per-instance materializer over `&[CaixaVersion]` (whose iterator
324/// yields `&CaixaVersion`, not `CaixaVersion`, so the paired
325/// owned-input [`From<CaixaVersion> for Cow<'static, str>`] axis
326/// alone forces every call site through an explicit `.clone()` /
327/// dereference restatement), a future
328/// `HashMap::<Cow<'static, str>, _>::from_iter` that keys off a
329/// borrowed-iteration axis where cloning the wrapper would allocate
330/// one [`String`] beyond the eventual [`Cow::Owned`] arm's own, a
331/// future generic
332/// `<T: for<'a> Into<Cow<'static, str>>>`-bound emitter on a
333/// per-caixa diagnostic column that walks the
334/// `iter().map(Into::into)` shape verbatim — reaches the wrapped
335/// byte-string through this one dispatch on the substrate primitive.
336///
337/// Deliberately returns [`std::borrow::Cow::Owned`] rather than
338/// [`std::borrow::Cow::Borrowed`] — the substrate-primitive
339/// [`CaixaVersion::as_str`] accessor's return does not carry the
340/// `&'static str` lifetime by construction, so the
341/// [`Cow<'static, str>`] output shape rules out the borrowed arm and
342/// the owned arm is the type-correct projection (mirroring the paired
343/// owned-input impl's own [`Cow::Owned`] discipline). Second corner
344/// on the `{Self, &Self} → Cow<'static, str>` reverse-projection
345/// family opened on the paired owned-input impl immediately above.
346/// Rust's `From` trait does not derive the `From<&Self>` sibling from
347/// a `From<Self>` impl (the blanket
348/// `impl<T, U> From<&T> for U where T: Clone, U: From<T>` does not
349/// exist in `core`), so every newtype that carries the owned-input
350/// reverse [`Cow<'static, str>`] axis but not the borrowed-input axis
351/// forces every borrowed call site through a `.clone()` /
352/// `<Cow<'static, str>>::from(v.clone())` detour whose type bounds
353/// have no compile-time link back to the newtype.
354///
355/// Pinned load-bearing by
356/// [`tests::caixa_version_from_borrowed_into_owned_cow_str_routes_through_as_str_accessor`]
357/// (byte-parity + [`Cow::Owned`]-arm pin against
358/// [`CaixaVersion::as_str`] via a borrowed input, plus a
359/// source-survival witness against silent move-out) and
360/// [`tests::caixa_version_from_owned_and_borrowed_into_cow_str_agree_on_every_shape`]
361/// (cross-axis partition pin against the paired owned-input impl on
362/// the same instance, closing the "owned-input move vs. borrowed-input
363/// clone" bifurcation on the same wrapped body through the
364/// [`Cow<'static, str>`] axis).
365impl From<&CaixaVersion> for std::borrow::Cow<'static, str> {
366 fn from(v: &CaixaVersion) -> std::borrow::Cow<'static, str> {
367 std::borrow::Cow::Owned(v.as_str().to_owned())
368 }
369}
370
371/// Canonical Zig-style git-tag prefix every `feira publish` run writes
372/// and every downstream consumer of a published caixa reads. A caixa
373/// published at `:versao "0.1.0"` lands as a git tag `v0.1.0` on the
374/// source repo's `origin` remote — the [`crate::CaixaVersion`] value
375/// gates the version body, this constant gates the prefix the body
376/// composes under.
377///
378/// Two production-code consumers carry this prefix on the same git
379/// remote axis:
380///
381/// 1. [`caixa-feira`]'s `feira publish` verb (caixa-feira/src/cmd/publish.rs)
382/// — the writer. Its `--prefix` clap flag defaults to this string
383/// and the verb computes the tag as `format!("{prefix}{versao}")`
384/// before `git tag -a <tag>` + `git push origin <tag>`.
385/// 2. [`caixa-flux`]'s [`caixa-flux::cluster_bundle`] renderer
386/// (caixa-flux/src/lib.rs) — the reader. Its
387/// `ClusterBundleOpts::for_caixa` constructor defaults
388/// `git_ref: GitRefSpec::Tag(...)` to `<prefix><versao>` so the
389/// rendered `gitrepository.yaml` carries `ref: { tag: v<versao> }`
390/// pointing `FluxCD`'s `GitRepository` reconciler at the exact tag
391/// the publisher just wrote.
392///
393/// Until this lift landed both consumers carried the bare `"v"` byte
394/// inline — `caixa-feira/src/cmd/publish.rs:22`'s clap
395/// `default_value = "v"` and `caixa-flux/src/lib.rs:335`'s
396/// `format!("v{}", caixa.versao)` literal. A future Zig-style-tag
397/// convention rebrand (the substrate moving to plain `<versao>` tags
398/// once the GitHub releases UI normalizes around the bare form, to
399/// `release/<versao>` once a sibling forge convention adopts the
400/// `<type>/<value>` slash-namespaced shape, or to a per-edition
401/// override the operator pins through a future `:placement
402/// :tag-prefix` slot) without a coordinated edit on both sides would
403/// silently emit a `feira publish`-side tag at one shape (e.g.
404/// `release/0.1.0`) and a `cluster_bundle`-side `ref: { tag: v0.1.0 }`
405/// pointing at the prior shape — Flux's `GitRepository` reconciler
406/// would loop forever looking for an upstream `v0.1.0` ref the publish
407/// remote no longer carries, the dependent `HelmRelease`'s `chart:
408/// sourceRef` would never resolve, every per-Servico apply would
409/// silently come up with the prior reconciled state, and the failure
410/// would surface at `kubectl describe gitrepository` time (the
411/// `Status: Stalled` / `Reason: Failed` arm) far from the rebrand
412/// commit's source.
413///
414/// Lifting the literal to one `&'static str` constant closes the drift
415/// footgun structurally — both consumers read from the same memory,
416/// so any future rebrand reaches both sites by construction and a CI
417/// build that re-introduces a sibling inline `"v"` literal trips the
418/// peer pinning tests
419/// ([`caixa-feira`]'s `publish_prefix_default_pins_lifted_caixa_core_constant`,
420/// [`caixa-flux`]'s `cluster_bundle_default_git_tag_uses_lifted_caixa_core_prefix`)
421/// at the build-time fail-before-deploy posture every prior
422/// load-bearing-string lift on this surface
423/// ([`crate::DEFAULT_NAMESPACE`] a085b26,
424/// [`crate::DEFAULT_LIBRARY_NAME`] 41438dc,
425/// [`crate::DEFAULT_SERVICO_PORT`] 1e22add) establishes.
426///
427/// Authoring-side `:versao` gates already refuse the `"v"`-prefixed
428/// publish tag shape leaking back into a version body — every typed
429/// `:versao` surface (top-level `:versao`, `:upgrade-from :from`,
430/// `:deps :versao`, `:deps-dev :versao`, `:membros :versao`,
431/// `:children :versao`) routes through `semver::Version::parse` /
432/// [`parse_requirement`], both of which reject the `v`-prefix as
433/// invalid `SemVer`. The split — bare `SemVer` at the `:versao` slot,
434/// `v<versao>` at the published git-tag axis — is the convention this
435/// constant pins.
436pub const DEFAULT_PUBLISH_TAG_PREFIX: &str = "v";
437
438/// Canonical git remote name every `feira` writer-side verb pushes to —
439/// the destination handle the operator-out-of-the-loop publish + deploy
440/// chain (`feira publish`, `feira deploy --apply`, `feira app deploy
441/// --apply`) names when it invokes `git push <remote> <ref>` against
442/// the local clone of the source / k8s GitOps repo.
443///
444/// Three production-code consumers carry this remote name on the same
445/// `git push` axis:
446///
447/// 1. [`caixa-feira`]'s `feira publish` verb (caixa-feira/src/cmd/publish.rs)
448/// — the writer-side publish path. Its `--remote` clap flag defaults
449/// to this string and the verb runs `git push <remote> <tag>` to push
450/// the freshly written `v<versao>` tag upstream.
451/// 2. [`caixa-feira`]'s `feira deploy --apply` verb
452/// (caixa-feira/src/cmd/deploy.rs) — the writer-side Servico cluster-
453/// deploy path. Its `push_origin` helper runs `git push origin HEAD`
454/// against the k8s GitOps repo's working tree after upserting the
455/// Servico's entry into the cluster's lareira-fleet-programs
456/// HelmRelease values.
457/// 3. [`caixa-feira`]'s `feira app deploy --apply` verb
458/// (caixa-feira/src/cmd/app.rs) — the writer-side Aplicacao
459/// cluster-deploy path. Its `push_origin` helper runs the same
460/// `git push origin HEAD` against the k8s GitOps repo after writing
461/// the rendered multi-doc YAML (programs.yaml entries + Cilium
462/// NetworkPolicies + Gateway/HTTPRoute) to the cluster's tree.
463///
464/// Until this lift landed all three consumers carried the bare
465/// `"origin"` byte inline — `publish.rs`'s clap `default_value = "origin"`,
466/// `deploy.rs`'s `git(repo, ["push", "origin", "HEAD"])`, and
467/// `app.rs`'s `git(repo, ["push", "origin", "HEAD"])`. A future
468/// remote-naming-convention rebrand on any one side (the substrate
469/// moving to `upstream` for forge-mirror clusters, to a per-tenant
470/// remote naming convention once the operator-flux pipeline grows the
471/// `:placement :remote` slot, or to the canonical multi-remote
472/// `release` + `mirror` split every Erlang/OTP `release_handler` /
473/// `relup` shop converges on once their git surface grows past one
474/// upstream) without a coordinated edit on the other two would have
475/// silently emitted a `git push` against a remote that doesn't exist
476/// on the operator's clone (`fatal: '<remote>' does not appear to be
477/// a git repository`) on one writer verb while the other two still
478/// pushed to the old remote — operator-observed symptom: the publish
479/// landed but the deploy didn't, or vice-versa, with the failure
480/// surfacing as a partial-state rollout far from the rebrand commit's
481/// source.
482///
483/// Lifting the literal to one `&'static str` constant closes the drift
484/// footgun structurally — all three consumers read from the same
485/// memory, so any future remote-naming rebrand reaches every writer
486/// verb by construction and a CI build that re-introduces a sibling
487/// inline `"origin"` literal trips the peer pinning tests
488/// ([`caixa-feira`]'s `publish_remote_default_pins_lifted_caixa_core_constant`
489/// on the clap-default axis, the sibling structural pins on the two
490/// `push_origin` helpers) at the build-time fail-before-deploy
491/// posture every prior load-bearing-string lift on this surface
492/// ([`crate::DEFAULT_NAMESPACE`] a085b26, [`crate::DEFAULT_LIBRARY_NAME`]
493/// 41438dc, [`crate::DEFAULT_SERVICO_PORT`] 1e22add,
494/// [`crate::DEFAULT_PUBLISH_TAG_PREFIX`] 0a6a602,
495/// [`crate::DEFAULT_FLUX_SYSTEM_NAMESPACE`] 7197d38) establishes.
496///
497/// Pairs with [`DEFAULT_PUBLISH_TAG_PREFIX`] on the same git remote
498/// axis — `feira publish` runs `git push <DEFAULT_GIT_REMOTE>
499/// <DEFAULT_PUBLISH_TAG_PREFIX><versao>` to push the typed `:versao`
500/// body composed under the canonical prefix to the canonical remote.
501/// Both halves of the publish-side convention now live in one place.
502pub const DEFAULT_GIT_REMOTE: &str = "origin";
503
504/// Canonical GitHub org name the pleme-io substrate defaults every un-
505/// pinned caixa's source repo to — the org handle the two substrate-side
506/// "no `:repositorio` / no `:fonte` declared, fall back to the canonical
507/// org" paths compose their `github:<org>/<nome>` shorthand + full
508/// `https://github.com/<org>/<nome>` URL under.
509///
510/// Two production-code consumers carry this org name on the same
511/// canonical-substrate-default-git-org axis:
512///
513/// 1. [`caixa-feira`]'s `feira lock` verb's `resolve_stub` (caixa-feira/src/cmd/lock.rs)
514/// — the resolver-side default. When a declared dep has no
515/// `:fonte` block the stub resolver composes
516/// `caixa_core::DepSource::default_github(<org>, &dep.nome)` to fill
517/// the shorthand `github:<org>/<nome>` fallback the phase 1.B
518/// `feira resolve` walker will resolve against upstream.
519/// 2. [`caixa-flux`]'s [`caixa-flux::cluster_bundle`] renderer
520/// (caixa-flux/src/lib.rs) — the renderer-side default. Its
521/// `ClusterBundleOpts::for_caixa` constructor defaults
522/// `git_url` to `format!("https://github.com/{org}/{}", caixa.nome)`
523/// when the caixa carries no `:repositorio`, so the rendered
524/// `gitrepository.yaml` points `FluxCD`'s `GitRepository`
525/// reconciler at the substrate's canonical git host for un-pinned
526/// caixas.
527///
528/// Until this lift landed both consumers carried the bare `"pleme-io"`
529/// byte inline — `caixa-feira/src/cmd/lock.rs:61`'s
530/// `default_github("pleme-io", …)` call and `caixa-flux/src/lib.rs`'s
531/// `format!("https://github.com/pleme-io/{}", …)` literal. A future
532/// substrate-side git-org migration (the pleme-io org renaming to a
533/// short form, forking to a per-tenant `<org>-<tenant>` shape once the
534/// operator-flux pipeline grows a `:placement :org` slot, or moving to
535/// a self-hosted forge under a wholly-owned org name once the
536/// substrate's forge-gen roadmap graduates past GitHub) without a
537/// coordinated edit on both sides would silently emit a `feira lock`-
538/// side `github:<old-org>/<nome>` fallback shorthand while the
539/// `cluster_bundle`-side `gitrepository.yaml` pointed at the new org's
540/// `<nome>` — the phase 1.B `feira resolve` walker would probe the
541/// prior org's git host for a repo that migrated with the org, or vice-
542/// versa: Flux's `GitRepository` reconciler would loop forever looking
543/// for an upstream repo the old org handle no longer maps to, the
544/// dependent `HelmRelease`'s `chart: sourceRef` would never resolve,
545/// every per-Servico apply would silently come up with the prior
546/// reconciled state, and the failure would surface at `kubectl describe
547/// gitrepository` time (the `Status: Stalled` / `Reason: Failed` arm)
548/// far from the org-migration commit's source.
549///
550/// Lifting the literal to one `&'static str` constant closes the drift
551/// footgun structurally — both consumers read from the same memory, so
552/// any future org migration reaches both sites by construction and a CI
553/// build that re-introduces a sibling inline `"pleme-io"` literal trips
554/// the peer pinning tests at the build-time fail-before-deploy posture
555/// every prior load-bearing-string lift on this surface
556/// ([`crate::DEFAULT_NAMESPACE`] a085b26,
557/// [`crate::DEFAULT_LIBRARY_NAME`] 41438dc,
558/// [`crate::DEFAULT_SERVICO_PORT`] 1e22add,
559/// [`DEFAULT_PUBLISH_TAG_PREFIX`] 0a6a602,
560/// [`DEFAULT_GIT_REMOTE`],
561/// [`crate::DEFAULT_FLUX_SYSTEM_NAMESPACE`] 7197d38) establishes.
562///
563/// Distinct from the [`crate::PLEME_LABEL_PREFIX`] canonical pleme-io
564/// label-namespace prefix (`"pleme.pleme.io"`, the K8s label-namespace
565/// axis every substrate-emitted cluster object's `LABEL_APLICACAO` /
566/// `LABEL_PROGRAM` / `LABEL_CONTRATO` axis shares) — these constants
567/// sit on separate schema-contract surfaces (the git-host org handle
568/// vs. the K8s label-namespace prefix) governed by independent rebrand
569/// cycles, so a git-org rename must not couple the K8s label-namespace
570/// axis to the git-host axis (or vice-versa). Splitting the two lets
571/// each schema's future rebrand land independently at its canonical
572/// const definition without silently coupling the surfaces — same
573/// "byte-distinct, semantically distinct" discipline the
574/// [`crate::PLEME_LABEL_PREFIX`] / [`crate::LABEL_APLICACAO`] /
575/// [`crate::LABEL_PROGRAM`] / [`crate::LABEL_CONTRATO`] set establishes
576/// on the peer per-K8s-label-namespace canonical-string surface.
577pub const DEFAULT_PLEME_GIT_ORG: &str = "pleme-io";
578
579/// Parse a dep's `:versao` string as a [`semver::VersionReq`].
580///
581/// Treats the literal `"*"` as "any version" (semver's wildcard).
582pub fn parse_requirement(s: &str) -> Result<semver::VersionReq, VersionError> {
583 if s == "*" {
584 return Ok(semver::VersionReq::STAR);
585 }
586 semver::VersionReq::parse(s).map_err(|e| VersionError::requirement(s, e.to_string()))
587}
588
589#[derive(Debug, Error, PartialEq, Eq)]
590pub enum VersionError {
591 #[error("invalid version '{0}': {1}")]
592 Semver(String, String),
593 #[error("invalid version requirement '{0}': {1}")]
594 Requirement(String, String),
595}
596
597// Fold the sole `VersionError::Semver(<into-String-expr>, <into-String-expr>)`
598// wire-up site on [`CaixaVersion::parse`]'s [`semver::Version::parse`]
599// `map_err` arm onto one substrate primitive — the paired
600// `(String, String)` two-slot tuple-newtype [`VersionError::Semver`] on
601// the [`CaixaVersion`] parser surface, the first of the two variants on
602// the [`VersionError`] envelope's paired `(String, String)` tuple-newtype
603// codec-magnitude family (its peer is [`VersionError::Requirement`] on
604// the sibling [`parse_requirement`] surface). Same discipline the peer
605// per-variant lifts on [`AplicacaoError`] / [`SupervisorError`] /
606// [`UpgradeError`] / [`LayoutError`] / [`DepError`] / [`ManifestError`]
607// / [`LimitsError`] / [`BehaviorError`] / [`DialetoError`] have
608// converged through the "one substrate primitive per emit-site variant"
609// ratchet: the sole wire-up site opens the identical
610// `VersionError::Semver(<into-String-expr>, <into-String-expr>)` block
611// against the parser-scoped `String` binding (`self.0.clone()`) and the
612// derived `String` binding (`e.to_string()`) on the failing
613// [`semver::Version::parse`] arm, so the fold routes the site through
614// one dispatch on a uniform pair of `impl Into<String>` params,
615// byte-equal to the pre-lift tuple-newtype construction on the same
616// arguments. The `impl Into<String>` bound covers both the pre-lift
617// `String` bindings and any future `&str` binding a downstream consumer
618// might carry without forcing the caller to spell the `.into()`
619// conversion at the wire-up site — the same shape the peer
620// [`LimitsError::empty_byte_size`] / [`LimitsError::empty_duration`] /
621// [`DialetoError::leitura`] folds carry on the single-slot `(String)`
622// tuple-newtype cousins of the same tuple-newtype error-envelope family
623// on the sibling parser surfaces. `#[must_use]` fires a compile warning
624// at any wire-up that mistakenly discards the constructed error. The
625// added [`PartialEq`] / [`Eq`] derives on the envelope (peer with the
626// sibling [`LimitsError`] / [`DialetoError`] / [`DepError`] envelopes
627// on the same axis) let the fail-before-pass-after byte-equality pins
628// below trip a de-lift regression at caixa-core test time under
629// `PartialEq` rather than at a downstream diagnostic shape drift.
630//
631// Every future consumer that wants to construct this variant outside
632// [`CaixaVersion::parse`] (a deferred `feira lint --canonical-versao`
633// per-caixa admission verb probing each authored top-level `:versao`
634// value against the same [`semver::Version::parse`] gate, an M4 typed
635// `mesh.pleme.io/v1alpha1/Servico` CR materializer's per-manifest
636// admission validator re-checking one edited `:versao` slot against
637// the [`CaixaVersion::parse`] semver floor, a per-`caixa.lisp` value-
638// shape pre-emitter probing each declared `:versao` magnitude ahead of
639// the operator's admit-cycle) now reaches the variant through one call
640// rather than re-inlining the two-slot tuple-newtype block in lockstep.
641impl VersionError {
642 /// Construct a [`VersionError::Semver`] carrying the offending
643 /// authoring string `value` and the underlying [`semver::Version::parse`]
644 /// `reason` verbatim in the variant's two-slot tuple-newtype payload.
645 /// Folds the uniform `Self::Semver(value.into(), reason.into())`
646 /// tuple-newtype construction onto one substrate primitive so every
647 /// wire-up on the variant reads through one dispatch rather than the
648 /// pre-lift open-coded
649 /// `VersionError::Semver(<into-String-expr>, <into-String-expr>)`
650 /// block. The paired `impl Into<String>` bounds cover the pre-lift
651 /// `String` wire-up shape on [`CaixaVersion::parse`]
652 /// (`self.0.clone()` on the parser-scoped `String` field, `e.to_string()`
653 /// on the derived `String` from the failing
654 /// [`semver::Version::parse`] arm) without forcing the caller to
655 /// spell the conversion at the wire-up site. Peer to the sibling
656 /// [`VersionError::Requirement`] variant on the [`parse_requirement`]
657 /// surface — the same `(String, String)` two-slot tuple-newtype axis
658 /// of the paired [`VersionError`] envelope, but on the `SemVer`
659 /// version-body parser surface rather than the version-requirement
660 /// parser surface.
661 #[must_use]
662 pub fn semver(value: impl Into<String>, reason: impl Into<String>) -> Self {
663 Self::Semver(value.into(), reason.into())
664 }
665
666 /// Construct a [`VersionError::Requirement`] carrying the offending
667 /// authoring string `value` and the underlying
668 /// [`semver::VersionReq::parse`] `reason` verbatim in the variant's
669 /// two-slot tuple-newtype payload. Folds the uniform
670 /// `Self::Requirement(value.into(), reason.into())` tuple-newtype
671 /// construction onto one substrate primitive so every wire-up on the
672 /// variant reads through one dispatch rather than the pre-lift open-
673 /// coded `VersionError::Requirement(<into-String-expr>,
674 /// <into-String-expr>)` block. Peer to the sibling
675 /// [`VersionError::semver`] ctor on the [`CaixaVersion::parse`]
676 /// surface — the same `(String, String)` two-slot tuple-newtype axis
677 /// of the paired [`VersionError`] envelope, but on the version-
678 /// requirement parser surface rather than the semver-version-body
679 /// parser surface. Closes the last un-lifted variant on the
680 /// [`VersionError`] envelope: every arm now reaches its emit site
681 /// through one substrate-primitive dispatch, matching the "one
682 /// substrate primitive per emit-site variant" ratchet the peer per-
683 /// variant lifts on [`crate::AplicacaoError`] /
684 /// [`crate::SupervisorError`] / [`crate::UpgradeError`] /
685 /// [`crate::LayoutError`] / [`crate::DepError`] /
686 /// [`crate::ManifestError`] / [`crate::LimitsError`] /
687 /// [`crate::BehaviorError`] / [`crate::DialetoError`] have converged
688 /// onto.
689 #[must_use]
690 pub fn requirement(value: impl Into<String>, reason: impl Into<String>) -> Self {
691 Self::Requirement(value.into(), reason.into())
692 }
693}
694
695#[cfg(test)]
696mod tests {
697 use super::*;
698
699 #[test]
700 fn version_round_trip() {
701 let v: CaixaVersion = "1.2.3".into();
702 assert_eq!(v.as_str(), "1.2.3");
703 assert_eq!(v.parse().unwrap().to_string(), "1.2.3");
704 }
705
706 #[test]
707 fn caixa_version_as_str_accessor_is_const_fn() {
708 // Fail-before-pass-after pin on [`CaixaVersion::as_str`]'s
709 // `const`-eval-surface posture. The accessor projects the typed
710 // newtype's inner [`String`] through the `pub const fn`
711 // [`String::as_str`] (const-stable since Rust 1.87, well within
712 // the workspace MSRV) — any future accidental downgrade to
713 // non-`const` fails `as_str_via_const_fn` at caixa-core build
714 // time with E0015 (`cannot call non-const method`), strictly
715 // stronger than a runtime `assert!`. Sibling of the peer
716 // per-M2/M3/universal-axis `String → &str` scalar-accessor
717 // family pins on the sibling `const`-eval-surface passes
718 // ([`crate::Caixa::nome`] / [`crate::Caixa::versao`] at the
719 // top-level manifest, [`crate::aplicacao::Membro::nome`] /
720 // [`crate::aplicacao::Membro::versao_requirement`] at the M3
721 // membership axis, [`crate::aplicacao::Entrada::hostname`] /
722 // [`crate::aplicacao::Entrada::destination`] at the M3 ingress
723 // axis, [`crate::supervisor::ChildSpec::nome`] /
724 // [`crate::supervisor::ChildSpec::versao_requirement`] at the
725 // M2 supervisor-tree axis,
726 // [`crate::upgrade::UpgradeFromEntry::prior_versao`] at the M2
727 // upgrade axis, [`crate::dep::Dep::nome`] /
728 // [`crate::dep::Dep::versao_requirement`] at the dep-graph
729 // axis, and the peer per-`:contratos` [`crate::aplicacao::WitContract::source`] /
730 // [`crate::aplicacao::WitContract::destination`] /
731 // [`crate::aplicacao::WitContract::world_ref`] trio the
732 // sibling pin at 279823b already anchors).
733 const fn as_str_via_const_fn(v: &CaixaVersion) -> &str {
734 v.as_str()
735 }
736 for versao in ["0.1.0", "1.2.3-alpha.1", "0.0.0", ""] {
737 let v: CaixaVersion = versao.into();
738 assert_eq!(as_str_via_const_fn(&v), v.as_str());
739 assert_eq!(v.as_str(), versao);
740 }
741 }
742
743 #[test]
744 fn star_is_any() {
745 let r = parse_requirement("*").unwrap();
746 assert!(r.matches(&"0.1.0".parse().unwrap()));
747 assert!(r.matches(&"99.0.0".parse().unwrap()));
748 }
749
750 #[test]
751 fn caret_matches_minor_range() {
752 let r = parse_requirement("^0.1").unwrap();
753 assert!(r.matches(&"0.1.0".parse().unwrap()));
754 assert!(r.matches(&"0.1.99".parse().unwrap()));
755 assert!(!r.matches(&"0.2.0".parse().unwrap()));
756 }
757
758 #[test]
759 fn invalid_version_errors() {
760 let v: CaixaVersion = "not-a-version".into();
761 assert!(v.parse().is_err());
762 }
763
764 #[test]
765 fn semver_ctor_matches_tuple_literal_wrap_on_str_binding() {
766 // Fail-before-pass-after byte-equality pin: the lifted
767 // [`VersionError::semver`] inherent ctor projects a `&str`
768 // binding pair through the paired `impl Into<String>` bounds
769 // byte-equal to the pre-lift open-coded
770 // `VersionError::Semver(<into-String-expr>, <into-String-expr>)`
771 // tuple-literal on the same fixture, so any future silent
772 // regression that swaps `.into()` for a divergent conversion
773 // (a stray `String::from(str::trim(v))` normalization, a
774 // parity-lossy `.to_lowercase()` fold, a `Cow<'_, str>` detour)
775 // trips at caixa-core test time under `PartialEq` rather than
776 // at a downstream diagnostic-shape drift on a consumer surface.
777 // Same shape the peer
778 // [`crate::LimitsError::empty_byte_size_ctor_matches_tuple_literal_wrap_on_str_binding`]
779 // / [`crate::DialetoError::leitura_ctor_matches_tuple_literal_wrap_on_str_binding`]
780 // pins carry on the sibling single-slot `(String)` tuple-newtype
781 // cousins of the same tuple-newtype error-envelope family on the
782 // sibling parser surfaces.
783 let value: &str = "not-a-version";
784 let reason: &str = "unexpected character 'n' while parsing major version number";
785 assert_eq!(
786 VersionError::semver(value, reason),
787 VersionError::Semver(value.to_string(), reason.to_string()),
788 "generated semver ctor over `&str` bindings must match \
789 the pre-lift tuple-literal wrap on the same fixture",
790 );
791 }
792
793 #[test]
794 fn semver_ctor_matches_tuple_literal_wrap_on_string_binding() {
795 // Fail-before-pass-after byte-equality pin on the paired owned-
796 // `String` shape — the actual wire-up shape on
797 // [`CaixaVersion::parse`] (`self.0.clone()` +
798 // `e.to_string()`). Peer to the `&str` variant above; refuses
799 // any future de-lift that inlines a divergent construction on
800 // the owned-`String` path (a stray `.trim().to_string()`
801 // normalization on either slot, a swap that routes the ctor
802 // through the sibling [`VersionError::Requirement`] variant on
803 // the paired parser surface).
804 let value: String = String::from("1.2");
805 let reason: String =
806 String::from("unexpected end of input while parsing minor version number");
807 assert_eq!(
808 VersionError::semver(value.clone(), reason.clone()),
809 VersionError::Semver(value, reason),
810 "generated semver ctor over owned-`String` bindings must \
811 match the pre-lift tuple-literal wrap on the same fixture",
812 );
813 }
814
815 #[test]
816 fn parse_semver_error_routes_through_semver_ctor() {
817 // Fail-before-pass-after routes-through pin: refuses any future
818 // de-lift of [`CaixaVersion::parse`]'s
819 // [`semver::Version::parse`] `map_err` arm off the substrate
820 // primitive. Sweeps three malformed authoring shapes (a bare
821 // non-numeric, a partial `major.minor` shape, a stray leading
822 // `v`-prefix that the [`DEFAULT_PUBLISH_TAG_PREFIX`] git-tag
823 // convention rejects at the version-body slot) through the
824 // parser and asserts the emitted [`VersionError`] equals the
825 // ctor-built error verbatim under `PartialEq`, so any future
826 // swap of the wire-up (an inline `Self::Semver(...)`
827 // re-inlining, a routing detour through the sibling
828 // [`VersionError::Requirement`] variant on the paired parser
829 // surface, a swap of the ordering on the paired arguments)
830 // trips at caixa-core test time rather than at a downstream
831 // diagnostic drift on a `feira lint` / operator admission
832 // callsite.
833 for bad in ["not-a-version", "1.2", "v0.1.0"] {
834 let v: CaixaVersion = bad.into();
835 let err = v
836 .parse()
837 .expect_err("malformed versao fixture must fail semver parsing");
838 let semver_reason = match semver::Version::parse(bad) {
839 Err(e) => e.to_string(),
840 Ok(_) => unreachable!(
841 "fixture `{bad}` is documented as a `SemVer` \
842 rejection but parsed cleanly — the pin's oracle \
843 drifted from `semver`'s current shape",
844 ),
845 };
846 assert_eq!(
847 err,
848 VersionError::semver(bad, semver_reason),
849 "CaixaVersion::parse must route its semver `map_err` \
850 arm through the lifted VersionError::semver ctor on \
851 the same offending value and semver reason",
852 );
853 }
854 }
855
856 #[test]
857 fn default_git_remote_pins_canonical_origin_byte() {
858 // Bridge-arm pin: [`DEFAULT_GIT_REMOTE`] resolves to the
859 // canonical `"origin"` byte today, the same remote-handle every
860 // `git clone <url>` invocation populates by default and every
861 // peer `feira` writer-side verb (`feira publish`, `feira deploy
862 // --apply`, `feira app deploy --apply`) names when it invokes
863 // `git push <remote> <ref>` against the local clone. Pin the
864 // literal here (peer with the
865 // [`DEFAULT_PUBLISH_TAG_PREFIX`] / [`crate::DEFAULT_SERVICO_PORT`]
866 // / [`crate::DEFAULT_NAMESPACE`] / [`crate::DEFAULT_LIBRARY_NAME`]
867 // / [`crate::DEFAULT_FLUX_SYSTEM_NAMESPACE`] canonical-literal
868 // pins on the sibling lifted-constant surfaces) so a future
869 // remote-naming rebrand surfaces here as a coordinated edit-
870 // point: the sibling [`caixa-feira`]
871 // `publish_remote_default_pins_lifted_caixa_core_constant`
872 // pinning test already pins the equality at the clap-default
873 // axis; this pin closes the second coordinate of the
874 // triangle by anchoring the lifted constant's current byte
875 // to the canonical git-default-remote convention's documented
876 // shape.
877 assert_eq!(DEFAULT_GIT_REMOTE, "origin");
878 }
879
880 #[test]
881 fn default_pleme_git_org_pins_canonical_pleme_io_byte() {
882 // Bridge-arm pin: [`DEFAULT_PLEME_GIT_ORG`] resolves to the
883 // canonical `"pleme-io"` GitHub-org-handle today, the same org
884 // name every peer substrate-side default-git-source consumer
885 // ([`caixa-feira`]'s `feira lock` `resolve_stub` for the
886 // per-dep `:fonte`-elided `github:<org>/<nome>` fallback,
887 // [`caixa-flux`]'s `ClusterBundleOpts::for_caixa` constructor
888 // for the per-caixa `:repositorio`-elided
889 // `https://github.com/<org>/<nome>` fallback) fills into its
890 // per-consumer render/resolve compose site. Pin the literal
891 // here (peer with the [`DEFAULT_PUBLISH_TAG_PREFIX`] /
892 // [`DEFAULT_GIT_REMOTE`] canonical-literal pins on the sibling
893 // lifted-constant surfaces) so a future substrate-side git-org
894 // migration surfaces here as a coordinated edit-point: both
895 // sibling consumer sites already thread through the same
896 // `&'static str`, this pin anchors the lifted constant's
897 // current byte to the canonical substrate-git-org convention's
898 // documented shape.
899 assert_eq!(DEFAULT_PLEME_GIT_ORG, "pleme-io");
900 }
901
902 #[test]
903 fn requirement_ctor_matches_tuple_literal_wrap_on_str_binding() {
904 // Fail-before-pass-after byte-equality pin: the lifted
905 // [`VersionError::requirement`] inherent ctor projects a `&str`
906 // binding pair through the paired `impl Into<String>` bounds
907 // byte-equal to the pre-lift open-coded
908 // `VersionError::Requirement(<into-String-expr>, <into-String-expr>)`
909 // tuple-literal on the same fixture. Same shape the peer
910 // [`VersionError::semver_ctor_matches_tuple_literal_wrap_on_str_binding`]
911 // pin carries on the sibling [`VersionError::Semver`] variant of
912 // the same `(String, String)` two-slot tuple-newtype envelope.
913 let value: &str = "not-a-req";
914 let reason: &str = "unexpected character 'n' while parsing major version number";
915 assert_eq!(
916 VersionError::requirement(value, reason),
917 VersionError::Requirement(value.to_string(), reason.to_string()),
918 "generated requirement ctor over `&str` bindings must match \
919 the pre-lift tuple-literal wrap on the same fixture",
920 );
921 }
922
923 #[test]
924 fn requirement_ctor_matches_tuple_literal_wrap_on_string_binding() {
925 // Fail-before-pass-after byte-equality pin on the paired owned-
926 // `String` shape. Peer to the `&str` variant above; refuses any
927 // future de-lift that inlines a divergent construction on the
928 // owned-`String` path (a stray `.trim().to_string()` normalization
929 // on either slot, a swap that routes the ctor through the sibling
930 // [`VersionError::Semver`] variant on the paired parser surface,
931 // an argument-ordering swap on the paired slots).
932 let value: String = String::from("^bogus");
933 let reason: String = String::from("unexpected character while parsing requirement");
934 assert_eq!(
935 VersionError::requirement(value.clone(), reason.clone()),
936 VersionError::Requirement(value, reason),
937 "generated requirement ctor over owned-`String` bindings must \
938 match the pre-lift tuple-literal wrap on the same fixture",
939 );
940 }
941
942 #[test]
943 fn parse_requirement_error_routes_through_requirement_ctor() {
944 // Fail-before-pass-after routes-through pin: refuses any future
945 // de-lift of [`parse_requirement`]'s
946 // [`semver::VersionReq::parse`] `map_err` arm off the substrate
947 // primitive. Sweeps three malformed authoring shapes (a bare
948 // non-numeric, a stray operator with no version body, a
949 // caret-prefixed non-numeric that the [`semver::VersionReq`]
950 // grammar rejects at the operator-body slot) through the parser
951 // and asserts the emitted [`VersionError`] equals the ctor-built
952 // error verbatim under `PartialEq`, so any future swap of the
953 // wire-up (an inline `Self::Requirement(...)` re-inlining, a
954 // routing detour through the sibling [`VersionError::Semver`]
955 // variant on the paired parser surface, an argument-ordering
956 // swap on the paired slots) trips at caixa-core test time rather
957 // than at a downstream diagnostic drift on a `feira lock` /
958 // resolver admission callsite. The `"*"` wildcard short-circuit
959 // is deliberately excluded from the sweep — it returns
960 // [`semver::VersionReq::STAR`] before reaching the parser arm.
961 for bad in ["not-a-req", "^", "^bogus"] {
962 let err = parse_requirement(bad)
963 .expect_err("malformed requirement fixture must fail parsing");
964 let semver_reason = match semver::VersionReq::parse(bad) {
965 Err(e) => e.to_string(),
966 Ok(_) => unreachable!(
967 "fixture `{bad}` is documented as a `VersionReq` \
968 rejection but parsed cleanly — the pin's oracle \
969 drifted from `semver`'s current shape",
970 ),
971 };
972 assert_eq!(
973 err,
974 VersionError::requirement(bad, semver_reason),
975 "parse_requirement must route its `map_err` arm through \
976 the lifted VersionError::requirement ctor on the same \
977 offending value and semver reason",
978 );
979 }
980 }
981
982 #[test]
983 fn default_publish_tag_prefix_pins_canonical_v_byte() {
984 // Bridge-arm pin: [`DEFAULT_PUBLISH_TAG_PREFIX`] resolves to the
985 // canonical Zig-style `"v"` byte today, the same prefix every
986 // peer doc-comment on the typed `:versao` surfaces (the
987 // top-level `:versao` `validate_versao` cascade at
988 // caixa-core/src/manifest.rs:646, the four sibling per-axis
989 // `:versao` requirement gates that name the publish-side
990 // `v<versao>` tag inline in their bodies) cites as the
991 // canonical convention. Pin the literal here (peer with the
992 // [`crate::DEFAULT_SERVICO_PORT`] / [`crate::DEFAULT_NAMESPACE`]
993 // / [`crate::DEFAULT_LIBRARY_NAME`] canonical-literal pins on
994 // the sibling lifted-constant surfaces) so a future rebrand of
995 // the constant surfaces here as a coordinated edit-point: both
996 // sibling pinning tests on the two consumer crates
997 // ([`caixa-feira`] `publish_prefix_default_pins_lifted_caixa_core_constant`,
998 // [`caixa-flux`] `cluster_bundle_default_git_tag_uses_lifted_caixa_core_prefix`)
999 // already pin the equality at the consumer-default axis; this
1000 // pin closes the third coordinate of the triangle by anchoring
1001 // the lifted constant's current byte to the canonical Zig-style
1002 // convention's documented shape.
1003 assert_eq!(DEFAULT_PUBLISH_TAG_PREFIX, "v");
1004 }
1005
1006 #[test]
1007 fn caixa_version_as_ref_str_routes_through_as_str_accessor() {
1008 // Fail-before-pass-after byte-parity pin on the lifted
1009 // `impl AsRef<str> for CaixaVersion` — asserts the standard-
1010 // library trait impl and the substrate-primitive
1011 // [`CaixaVersion::as_str`] `pub const fn` accessor resolve to
1012 // the same `&str` per instance, so any future silent detour
1013 // that routes the impl through a divergent projection (a
1014 // `Cow<'_, str>` intermediate, a stray `.to_lowercase()`
1015 // normalization, a swap onto a per-arm inline `&self.0.as_str()`
1016 // re-inlining, a swap onto a divergent [`String::trim`]
1017 // fold) trips at caixa-core test time under `PartialEq`
1018 // rather than at a downstream `impl AsRef<str>`-bound
1019 // consumer's silent split. Sweeps four authoring shapes (a
1020 // canonical release version, a pre-release build-metadata
1021 // version, the zero-version canonical unset baseline, and
1022 // the empty-string byte the caller-side default-construct
1023 // path composes) so every non-degenerate arm of the wrapped
1024 // `String` storage is covered. Peer of the sibling
1025 // [`caixa_version_as_str_accessor_is_const_fn`] const-eval
1026 // pin on the same [`CaixaVersion::as_str`] primitive — the
1027 // two pins together cover the const-eval axis (the pin above)
1028 // and the trait-projection axis (this pin) of the same
1029 // substrate-primitive scalar accessor.
1030 for versao in ["0.1.0", "1.2.3-alpha.1", "0.0.0", ""] {
1031 let v: CaixaVersion = versao.into();
1032 assert_eq!(
1033 <CaixaVersion as AsRef<str>>::as_ref(&v),
1034 v.as_str(),
1035 "AsRef<str> impl must byte-equal CaixaVersion::as_str \
1036 on the same instance — divergence signals a silent \
1037 detour off the substrate-primitive accessor",
1038 );
1039 assert_eq!(
1040 <CaixaVersion as AsRef<str>>::as_ref(&v),
1041 versao,
1042 "AsRef<str> impl must byte-equal the pre-lift wrapped \
1043 String storage on round-trip through the From<&str> \
1044 constructor — divergence signals a normalization \
1045 detour on either the constructor or the accessor",
1046 );
1047 }
1048 }
1049
1050 #[test]
1051 fn caixa_version_as_ref_str_routes_through_display_via_shared_accessor() {
1052 // Fail-before-pass-after byte-parity pin on the three-path
1053 // convergence discipline the substrate primitive now carries
1054 // on the `&str`-projection axis: `<CaixaVersion as
1055 // AsRef<str>>::as_ref(&v)` (the newly lifted impl),
1056 // `format!("{v}")` (the pre-existing [`fmt::Display`] impl),
1057 // and `v.as_str()` (the substrate-primitive `pub const fn`
1058 // accessor both trait impls delegate through) must resolve to
1059 // the same byte-string on every instance. Refuses any future
1060 // divergence between the two trait impls (a stray
1061 // [`fmt::Display::fmt`] rewrite that inlines
1062 // `f.write_str(&self.0)` on the wrapped `String` directly,
1063 // bypassing the shared accessor; a hypothetical `AsRef<str>`
1064 // rewrite that inlines the same `&self.0` field-access) that
1065 // would silently split the two projection paths of the same
1066 // typed newtype. Mirrors the sibling three-path-convergence
1067 // discipline the peer [`RestartStrategy`] typed enum carries
1068 // on its `Display` / `as_str` / `Serialize` triple (aplicacao.rs
1069 // pin `restart_strategy_display_matches_serialized_wire_byte_string`).
1070 for versao in ["0.1.0", "1.2.3-alpha.1", ""] {
1071 let v: CaixaVersion = versao.into();
1072 let via_as_ref: &str = <CaixaVersion as AsRef<str>>::as_ref(&v);
1073 let via_display: String = format!("{v}");
1074 let via_accessor: &str = v.as_str();
1075 assert_eq!(via_as_ref, via_accessor);
1076 assert_eq!(via_display, via_accessor);
1077 assert_eq!(via_as_ref, via_display.as_str());
1078 }
1079 }
1080
1081 #[test]
1082 fn caixa_version_from_into_owned_string_returns_wrapped_body() {
1083 // Fail-before-pass-after byte-parity pin on the lifted
1084 // `impl From<CaixaVersion> for String` — asserts the owned-input
1085 // reverse-projection routes the wrapper's own heap allocation
1086 // through verbatim (no re-copy, no normalization detour) so
1087 // `String::from(v)` returns the same bytes `v.as_str()`
1088 // borrows. Refuses any future silent detour that would swap
1089 // the move on `v.0` for an allocating `.as_str().to_owned()` /
1090 // `.to_string()` cascade (the pre-lift compose shape), a stray
1091 // `.trim().to_owned()` normalization, or a routing through the
1092 // sibling [`fmt::Display`] emitter that would introduce a
1093 // formatter round-trip.
1094 for versao in ["0.1.0", "1.2.3-alpha.1", "0.0.0", ""] {
1095 let v: CaixaVersion = versao.into();
1096 let expected = v.as_str().to_owned();
1097 let owned: String = String::from(v);
1098 assert_eq!(
1099 owned, expected,
1100 "String::from(v) must return the wrapper's own bytes verbatim",
1101 );
1102 assert_eq!(
1103 owned, versao,
1104 "String::from(v) must round-trip byte-equal through the From<&str> constructor",
1105 );
1106 }
1107 }
1108
1109 #[test]
1110 fn caixa_version_from_into_owned_string_and_as_str_agree_on_every_shape() {
1111 // Fail-before-pass-after cross-axis partition pin: the owned-
1112 // input [`From<CaixaVersion> for String`] reverse projection
1113 // and the borrowed [`AsRef<str>`] projection resolve to the
1114 // same bytes on every instance, and the paired forward
1115 // [`From<String> for CaixaVersion`] constructor closes the
1116 // `Self → String → Self` round-trip by construction. Refuses
1117 // any future silent split between the owned-move reverse axis
1118 // and the borrowed-clone AsRef axis (a stray normalization on
1119 // one path only) that would let `String::from(v)` and
1120 // `v.as_ref::<str>()` diverge on the same instance.
1121 for versao in ["0.1.0", "1.2.3-alpha.1", "0.0.0", ""] {
1122 let v: CaixaVersion = versao.into();
1123 let via_as_ref: String = <CaixaVersion as AsRef<str>>::as_ref(&v).to_owned();
1124 let via_to_string: String = v.to_string();
1125 let via_from: String = String::from(v.clone());
1126 assert_eq!(via_from, via_as_ref);
1127 assert_eq!(via_from, via_to_string);
1128 let round_trip: CaixaVersion = via_from.clone().into();
1129 assert_eq!(round_trip, v);
1130 }
1131 }
1132
1133 #[test]
1134 fn caixa_version_from_borrowed_into_owned_string_routes_through_as_str_accessor() {
1135 // Fail-before-pass-after byte-parity pin on the lifted
1136 // `impl From<&CaixaVersion> for String` — asserts the
1137 // borrowed-input reverse projection allocates a fresh
1138 // [`String`] whose bytes byte-equal the substrate-primitive
1139 // [`CaixaVersion::as_str`] accessor on the same instance,
1140 // preserving the source [`CaixaVersion`] intact (no move-out).
1141 // Refuses any future silent detour that would route the impl
1142 // through a divergent projection (a stray normalization step,
1143 // a swap onto the sibling [`fmt::Display`]-routed
1144 // [`ToString::to_string`] surface, a re-inlining that
1145 // dereferences `&self.0` outside the shared accessor).
1146 for versao in ["0.1.0", "1.2.3-alpha.1", "0.0.0", ""] {
1147 let v: CaixaVersion = versao.into();
1148 let via_borrowed: String = String::from(&v);
1149 assert_eq!(
1150 via_borrowed,
1151 v.as_str(),
1152 "String::from(&v) must byte-equal CaixaVersion::as_str",
1153 );
1154 // The borrowed-input impl must not move out of the source.
1155 assert_eq!(
1156 v.as_str(),
1157 versao,
1158 "source CaixaVersion must survive borrowed-input projection"
1159 );
1160 }
1161 }
1162
1163 #[test]
1164 fn caixa_version_from_owned_and_borrowed_into_string_agree_on_every_shape() {
1165 // Fail-before-pass-after cross-axis partition pin: the paired
1166 // owned-input [`From<CaixaVersion> for String`] and
1167 // borrowed-input [`From<&CaixaVersion> for String`] impls
1168 // resolve to the same bytes on every instance, closing the
1169 // "owned-input move vs. borrowed-input clone" bifurcation on
1170 // the same wrapped body. Refuses any future silent split
1171 // between the two corners (a normalization on one path only, a
1172 // divergent routing that would let `String::from(v.clone())`
1173 // and `String::from(&v)` disagree on the same body).
1174 for versao in ["0.1.0", "1.2.3-alpha.1", "0.0.0", ""] {
1175 let v: CaixaVersion = versao.into();
1176 let via_borrowed: String = String::from(&v);
1177 let via_owned: String = String::from(v.clone());
1178 assert_eq!(via_owned, via_borrowed);
1179 assert_eq!(via_borrowed, versao);
1180 }
1181 }
1182
1183 #[test]
1184 fn caixa_version_from_into_owned_cow_str_returns_owned_wrapped_body() {
1185 // Fail-before-pass-after byte-parity + [`Cow::Owned`]-arm pin
1186 // on the lifted `impl From<CaixaVersion> for
1187 // std::borrow::Cow<'static, str>` — asserts the owned-input
1188 // reverse projection routes the wrapper's own heap allocation
1189 // through `Cow::Owned(v.0)` verbatim (no re-copy, no
1190 // normalization detour, no `Cow::Borrowed` misclassification
1191 // that would demand a `&'static str` the runtime wrapper cannot
1192 // carry), so the emitted [`Cow`] byte-equals the substrate-
1193 // primitive [`CaixaVersion::as_str`] accessor on the same
1194 // instance and round-trips byte-equal through the paired
1195 // forward [`From<String> for CaixaVersion`] constructor.
1196 // Refuses any future silent detour: a swap of the move on
1197 // `v.0` for an allocating `.as_str().to_owned()` cascade (the
1198 // pre-lift compose shape would double-allocate a fresh
1199 // intermediary [`String`] on the way to the same
1200 // [`Cow::Owned`] arm), a stray `.trim().to_owned()`
1201 // normalization, or a mis-routing through
1202 // [`Cow::Borrowed`] on a non-`'static` byte-string that would
1203 // not type-check.
1204 use std::borrow::Cow;
1205 for versao in ["0.1.0", "1.2.3-alpha.1", "0.0.0", ""] {
1206 let v: CaixaVersion = versao.into();
1207 let expected = v.as_str().to_owned();
1208 let cow: Cow<'static, str> = Cow::from(v.clone());
1209 assert!(
1210 matches!(cow, Cow::Owned(_)),
1211 "From<CaixaVersion> for Cow<'static, str> must land on \
1212 the Cow::Owned arm — a runtime String wrapper cannot \
1213 promise the 'static lifetime the Cow::Borrowed arm \
1214 requires",
1215 );
1216 assert_eq!(
1217 cow.as_ref(),
1218 expected,
1219 "Cow::from(v) must return the wrapper's own bytes verbatim",
1220 );
1221 let round_trip: CaixaVersion = cow.into_owned().into();
1222 assert_eq!(
1223 round_trip, v,
1224 "Cow::from(v).into_owned() must round-trip byte-equal \
1225 through the From<String> constructor",
1226 );
1227 }
1228 }
1229
1230 #[test]
1231 fn caixa_version_from_into_owned_cow_str_and_string_agree_on_every_shape() {
1232 // Fail-before-pass-after cross-axis partition pin: the owned-
1233 // input [`From<CaixaVersion> for Cow<'static, str>`] reverse
1234 // projection and the paired owned-input
1235 // [`From<CaixaVersion> for String`] reverse projection resolve
1236 // to the same bytes on every instance, and both agree with the
1237 // borrowed [`AsRef<str>`] surface on the same wrapped body.
1238 // Refuses any future silent split between the two owned-input
1239 // reverse-projection axes (a stray normalization on one path
1240 // only, a divergent routing that would let
1241 // `Cow::from(v.clone())` and `String::from(v.clone())` disagree
1242 // on the same body) that would silently split the same-shape
1243 // owned-move discipline across the two reverse-projection
1244 // targets.
1245 use std::borrow::Cow;
1246 for versao in ["0.1.0", "1.2.3-alpha.1", "0.0.0", ""] {
1247 let v: CaixaVersion = versao.into();
1248 let via_string: String = String::from(v.clone());
1249 let via_cow: Cow<'static, str> = Cow::from(v.clone());
1250 let via_as_ref: &str = <CaixaVersion as AsRef<str>>::as_ref(&v);
1251 assert_eq!(via_cow.as_ref(), via_string.as_str());
1252 assert_eq!(via_cow.as_ref(), via_as_ref);
1253 assert_eq!(via_cow.as_ref(), versao);
1254 }
1255 }
1256
1257 #[test]
1258 fn caixa_version_from_borrowed_into_owned_cow_str_routes_through_as_str_accessor() {
1259 // Fail-before-pass-after byte-parity + [`Cow::Owned`]-arm pin
1260 // on the lifted `impl From<&CaixaVersion> for
1261 // std::borrow::Cow<'static, str>` — asserts the borrowed-input
1262 // reverse projection allocates a fresh [`Cow::Owned`] whose
1263 // bytes byte-equal the substrate-primitive
1264 // [`CaixaVersion::as_str`] accessor on the same instance,
1265 // preserving the source [`CaixaVersion`] intact (no move-out).
1266 // Refuses any future silent detour that would route the impl
1267 // through a divergent projection (a stray normalization step,
1268 // a mis-routing onto [`Cow::Borrowed`] on a non-`'static`
1269 // byte-string that would not type-check, a re-inlining that
1270 // dereferences `&self.0` outside the shared accessor).
1271 use std::borrow::Cow;
1272 for versao in ["0.1.0", "1.2.3-alpha.1", "0.0.0", ""] {
1273 let v: CaixaVersion = versao.into();
1274 let via_borrowed: Cow<'static, str> = Cow::from(&v);
1275 assert!(
1276 matches!(via_borrowed, Cow::Owned(_)),
1277 "From<&CaixaVersion> for Cow<'static, str> must land on \
1278 the Cow::Owned arm — a runtime String wrapper cannot \
1279 promise the 'static lifetime the Cow::Borrowed arm \
1280 requires",
1281 );
1282 assert_eq!(
1283 via_borrowed.as_ref(),
1284 v.as_str(),
1285 "Cow::from(&v) must byte-equal CaixaVersion::as_str",
1286 );
1287 // The borrowed-input impl must not move out of the source.
1288 assert_eq!(
1289 v.as_str(),
1290 versao,
1291 "source CaixaVersion must survive borrowed-input projection",
1292 );
1293 }
1294 }
1295
1296 #[test]
1297 fn caixa_version_from_owned_and_borrowed_into_cow_str_agree_on_every_shape() {
1298 // Fail-before-pass-after cross-axis partition pin: the paired
1299 // owned-input [`From<CaixaVersion> for Cow<'static, str>`] and
1300 // borrowed-input [`From<&CaixaVersion> for Cow<'static, str>`]
1301 // impls resolve to the same bytes on every instance, closing
1302 // the "owned-input move vs. borrowed-input clone" bifurcation
1303 // on the same wrapped body through the [`Cow<'static, str>`]
1304 // axis. Refuses any future silent split between the two
1305 // corners (a normalization on one path only, a divergent
1306 // routing that would let `Cow::from(v.clone())` and
1307 // `Cow::from(&v)` disagree on the same body). Both corners
1308 // must land on [`Cow::Owned`] — the runtime wrapper's storage
1309 // rules out the borrowed arm on both input shapes alike.
1310 use std::borrow::Cow;
1311 for versao in ["0.1.0", "1.2.3-alpha.1", "0.0.0", ""] {
1312 let v: CaixaVersion = versao.into();
1313 let via_borrowed: Cow<'static, str> = Cow::from(&v);
1314 let via_owned: Cow<'static, str> = Cow::from(v.clone());
1315 assert!(matches!(via_borrowed, Cow::Owned(_)));
1316 assert!(matches!(via_owned, Cow::Owned(_)));
1317 assert_eq!(via_owned.as_ref(), via_borrowed.as_ref());
1318 assert_eq!(via_borrowed.as_ref(), versao);
1319 }
1320 }
1321}