caixa_core/manifest.rs
1use std::path::{Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4use tatara_lisp::DeriveTataraDomain;
5
6use thiserror::Error;
7
8use crate::{
9 CaixaKind, Dep,
10 behavior::BehaviorSpec,
11 dep::DepError,
12 limits::LimitsSpec,
13 render::{
14 PathShapeViolation, is_computeunit_yaml_extension, is_git_repo_url, is_lisp_extension,
15 is_sandboxed_relative_path,
16 },
17 supervisor::SupervisorSpec,
18 upgrade::UpgradeFromEntry,
19};
20
21/// Top-level manifest for a caixa (a tatara-lisp package).
22///
23/// Authored as `caixa.lisp`:
24///
25/// ```lisp
26/// (defcaixa
27/// :nome "pangea-tatara-aws"
28/// :versao "0.1.0"
29/// :kind Biblioteca
30/// :edicao "2026"
31/// :descricao "AWS provider caixa for tatara-lisp"
32/// :repositorio "github:pleme-io/pangea-tatara-aws"
33/// :licenca "MIT"
34/// :autores ("pleme-io")
35/// :etiquetas ("iac" "aws" "pangea")
36/// :deps ((:nome "caixa-teia" :versao "^0.1")
37/// (:nome "iac-forge-ir" :versao "^0.5"))
38/// :deps-dev ((:nome "tatara-check" :versao "*"))
39/// :bibliotecas ("lib/pangea-tatara-aws.lisp"))
40/// ```
41///
42/// Because `Caixa` derives [`tatara_lisp::domain::TataraDomain`], the manifest
43/// is parsed directly by the tatara-lisp compiler — an ill-formed manifest is
44/// a compile error, not a runtime error.
45#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone, PartialEq)]
46#[serde(rename_all = "camelCase")]
47#[tatara(keyword = "defcaixa")]
48pub struct Caixa {
49 /// Package name — the canonical string used in `:deps`, the registry, and
50 /// the default lib/exe entry names.
51 pub nome: String,
52
53 /// Package version — a semver literal like `"0.1.0"`. Parsed lazily via
54 /// [`crate::CaixaVersion::parse`].
55 pub versao: String,
56
57 /// What this caixa produces. See [`CaixaKind`].
58 pub kind: CaixaKind,
59
60 /// Language edition — determines macro surface + compatibility flags.
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub edicao: Option<String>,
63
64 /// Free-form description shown in the registry listing.
65 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub descricao: Option<String>,
67
68 /// Homepage or repo URL.
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub repositorio: Option<String>,
71
72 /// SPDX license expression — `"MIT"`, `"Apache-2.0 OR MIT"`, etc.
73 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub licenca: Option<String>,
75
76 /// Authors — free-form strings.
77 #[serde(default)]
78 pub autores: Vec<String>,
79
80 /// Topical tags used for registry search.
81 #[serde(default)]
82 pub etiquetas: Vec<String>,
83
84 /// Runtime dependencies.
85 #[serde(default)]
86 pub deps: Vec<Dep>,
87
88 /// Development-only dependencies (tests, lint, bench).
89 #[serde(default)]
90 pub deps_dev: Vec<Dep>,
91
92 /// Paths to executable entry points (relative to the package root).
93 /// Required when `:kind Binario`.
94 #[serde(default)]
95 pub exe: Vec<String>,
96
97 /// Paths to library entry points (relative to the package root).
98 /// First entry is the canonical `lib/<nome>.lisp`; when omitted under
99 /// `:kind Biblioteca`, the layout check expects `lib/<nome>.lisp`.
100 #[serde(default)]
101 pub bibliotecas: Vec<String>,
102
103 /// Paths to service manifests (relative to the package root).
104 /// Required when `:kind Servico`.
105 #[serde(default)]
106 pub servicos: Vec<String>,
107
108 // ── M2 typed-substrate extensions per theory/ABSORPTION-ROADMAP.md ──
109 //
110 // All four are optional + default to "absent"; existing caixas
111 // round-trip unchanged. Each maps onto a prior-art primitive named
112 // in theory/INSPIRATIONS.md:
113 //
114 // :limits — Lunatic per-process limits (§III.1)
115 // :behavior — OTP gen_server callbacks (§II.3)
116 // :upgrade-from — OTP appup migration (§II.4)
117 // :estrategia — OTP supervisor strategy (§II.2 + §III.2)
118 // :children — OTP supervisor children (§II.2 + §III.2)
119 //
120 // The supervisor slots are flat on Caixa (vs nested under a
121 // SupervisorSpec sub-form) to keep tatara-lisp authoring at one
122 // level of nesting; SupervisorSpec exists for validation +
123 // composition convenience (`Caixa::supervisor_view()`).
124 /// Lunatic-style per-process resource limits. None = unbounded.
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub limits: Option<LimitsSpec>,
127
128 /// OTP-shaped behavior callbacks for Servico-kind caixas.
129 /// Authored as `(:on-init "..." :on-call "..." …)`.
130 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub behavior: Option<BehaviorSpec>,
132
133 /// OTP appup — declarative upgrade instructions per prior version.
134 /// Empty list = no hot-upgrade path declared (caller falls back to
135 /// `:Restart` strategy).
136 #[serde(default)]
137 pub upgrade_from: Vec<UpgradeFromEntry>,
138
139 /// OTP supervisor strategy. Required when `:kind Supervisor`;
140 /// ignored otherwise.
141 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub estrategia: Option<crate::supervisor::RestartStrategy>,
143
144 /// Max restarts before the supervisor itself fails. Defaults via
145 /// SupervisorSpec at validation time.
146 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub max_restarts: Option<u32>,
148
149 /// Sliding window for `max_restarts`. Authored as a duration
150 /// string (`"60s"`, `"5m"`).
151 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub restart_window: Option<String>,
153
154 /// Static children of a supervisor. Required for OneForOne /
155 /// OneForAll / RestForOne; must be empty for SimpleOneForOne.
156 #[serde(default)]
157 pub children: Vec<crate::supervisor::ChildSpec>,
158
159 // ── M3 Aplicacao slots (theory/MESH-COMPOSITION.md) ─────────────────
160 //
161 // Required when :kind Aplicacao; ignored otherwise.
162 // Composed into a typed AplicacaoSpec via Caixa::aplicacao_view().
163 /// Member Servicos that make up this Aplicacao. Each is a
164 /// caixa-name + version-constraint pair. Required for Aplicacao.
165 #[serde(default)]
166 pub membros: Vec<crate::aplicacao::Membro>,
167
168 /// WIT-typed inter-Servico contracts. Each `:de` and `:para`
169 /// must reference a name in `:membros`.
170 #[serde(default)]
171 pub contratos: Vec<crate::aplicacao::WitContract>,
172
173 /// Mesh-level policies (timeout, retries, circuit-breaker, mTLS,
174 /// rate-limit). Apply to every contrato unless overridden per-edge
175 /// in M4.
176 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub politicas: Option<crate::aplicacao::MeshPolicy>,
178
179 /// Placement strategy across the cluster fleet
180 /// (single-node | replicated | sharded).
181 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub placement: Option<crate::aplicacao::Placement>,
183
184 /// External entry point — gateway / ingress shape. Optional;
185 /// only for public Aplicacaos.
186 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub entrada: Option<crate::aplicacao::Entrada>,
188
189 // ── Acao slot (CANTEIRO §7.1-C) ──────────────────────────────────────
190 //
191 // Required when :kind Acao; ignored otherwise (mirrors the M2/
192 // supervisor-tree/M3 slot triads above — a declared-but-foreign `:ci`
193 // is a `LayoutError::CiOnNonAcao` build error, not a silent drop).
194 /// Typed CI run — a repo's CI run as a set of typed nodes + their
195 /// dependency edges. Required for `:kind Acao`; validated (not
196 /// rendered) by the `caixa-actions` renderer via
197 /// `canteiro_types::decompose`. See `caixa-actions`' crate docs for
198 /// the M0 validate-only contract.
199 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub ci: Option<canteiro_types::CiRun>,
201}
202
203/// Why reading a manifest into a [`Caixa`] failed.
204///
205/// Split from [`ManifestError`] (which reports a *parsed* manifest that is
206/// semantically wrong) because the two answer different questions, and the
207/// distinction is the whole point of this type: `ManifestError` means "your
208/// caixa is wrong", `LeituraError::DialetoEstrangeiro` means "this file is not
209/// a caixa".
210#[derive(Debug, thiserror::Error)]
211pub enum LeituraError {
212 /// The source is not readable as a `(defcaixa …)` package manifest — bad
213 /// syntax, a wrong head symbol, an unknown or mistyped slot.
214 ///
215 /// `#[source]`, not `#[error(transparent)]`. Transparent delegates
216 /// `source()` past the inner error to ITS source, which drops the
217 /// `LispError` off the cause chain — and `feira`'s
218 /// `load_caixa_parse_error_preserves_underlying_lisp_error_on_chain`
219 /// pins that a caller can `downcast_ref::<tatara_lisp::LispError>()`
220 /// through an anyhow context to read the typed payload. That pin caught
221 /// this exact regression when the variant first landed transparent.
222 #[error("{0}")]
223 Leitura(
224 #[source]
225 #[from]
226 tatara_lisp::LispError,
227 ),
228
229 /// The source IS a well-formed `(defcaixa …)` form, but of a different
230 /// declaration than this crate's.
231 ///
232 /// The variant that did not exist before, and whose absence is the defect.
233 /// A `(defcaixa :name "x" :ecosystem :go …)` used to reach the derive's
234 /// `parse_kwargs_strict` and come back as an unknown-keyword rejection —
235 /// byte-identical in shape to a typo in a real manifest. Measured over the
236 /// org checkout on 2026-07-31, that shape is the MAJORITY of the corpus, so
237 /// the confusing error was also the common one.
238 ///
239 /// Carrying the dialect means a consumer can branch on "not mine" without
240 /// re-parsing, and a census can count it. Every user-facing byte-string
241 /// (canonical keyword, one-line description, consuming crate) is a
242 /// projection of [`crate::dialeto::CaixaDialeto`] — the variant stores the
243 /// typed dialect and the `#[error]` template calls
244 /// [`CaixaDialeto::palavra_canonica`] /
245 /// [`CaixaDialeto::descricao`] / [`CaixaDialeto::consumidor`] on it, so
246 /// the three axes cannot silently diverge from the classification. Prior
247 /// to this closure the variant carried each accessor's return value as a
248 /// stored `&'static str` snapshot alongside `dialeto`, and the sole
249 /// constructor at [`Caixa::from_lisp`] filled all four fields — a caller
250 /// could construct `DialetoEstrangeiro { dialeto: Molde,
251 /// palavra_canonica: "defcaixa", … }` and every downstream consumer
252 /// (Display, ad-hoc audit, future JSON serialization) would silently
253 /// disagree with `dialeto.palavra_canonica() == "defmolde"`. The typed
254 /// enum owns the projections; the variant only carries the axis.
255 #[error(
256 "this is a `{palavra}` declaration ({desc}), read by \
257 {cons} — not a caixa-core package manifest. `defcaixa` is the \
258 tatara-lisp package manifest (`:nome :versao :kind :deps …`); the two \
259 are different declarations that shared one keyword until 2026-07-31",
260 palavra = dialeto.palavra_canonica(),
261 desc = dialeto.descricao(),
262 cons = dialeto.consumidor()
263 )]
264 DialetoEstrangeiro {
265 /// Which declaration this actually is. Sole authoritative axis;
266 /// every user-facing projection routes through
267 /// [`crate::dialeto::CaixaDialeto`]'s typed accessors so the four
268 /// axes cannot silently disagree.
269 dialeto: crate::dialeto::CaixaDialeto,
270 },
271
272 /// Not a manifest declaration at all.
273 #[error(transparent)]
274 Dialeto(#[from] crate::dialeto::DialetoError),
275}
276
277impl Caixa {
278 /// Parse a `caixa.lisp` source string to a typed `Caixa`.
279 ///
280 /// Classifies the dialect **before** parsing. A `(defcaixa …)` of another
281 /// declaration is [`LeituraError::DialetoEstrangeiro`], naming what it is
282 /// and who reads it, instead of an unknown-keyword rejection that reads as
283 /// "your manifest is broken".
284 ///
285 /// The ordering is load-bearing. Handing a foreign dialect to the derive
286 /// first and interpreting the failure afterwards would mean guessing from
287 /// an error message, and the guess would be wrong for every file whose
288 /// first unknown slot happens to be one both schemas could plausibly carry.
289 pub fn from_lisp(src: &str) -> Result<Self, LeituraError> {
290 use tatara_lisp::domain::TataraDomain;
291 let forms = tatara_lisp::read(src).map_err(LeituraError::Leitura)?;
292 let first = forms.first().ok_or(crate::dialeto::DialetoError::Vazio)?;
293
294 // Route the foreign-dialect rejection gate through the lifted
295 // [`crate::dialeto::CaixaDialeto::is_molde_family`] (e9d2315)
296 // typed predicate rather than the pre-lift hand-rolled three-arm
297 // `match { Pacote => {}, Desconhecido => {}, foreign => Err(…) }`
298 // literal — the `defmolde` declaration-family partition (the two-
299 // arity closure of [`crate::dialeto::CaixaDialeto::Molde`] and
300 // [`crate::dialeto::CaixaDialeto::MoldePosicional`], the two arms
301 // whose sibling [`crate::dialeto::CaixaDialeto::palavra_canonica`]
302 // projection already collapses onto `"defmolde"` and whose sibling
303 // [`crate::dialeto::CaixaDialeto::consumidor`] projection already
304 // collapses onto `"pleme-doc-gen"`) resolves through one dispatch
305 // on the substrate primitive. `Pacote` (the tatara-lisp package
306 // manifest this derive can parse) and `Desconhecido` (deliberately
307 // falls through to the derive rather than short-circuiting: a
308 // `(defcaixa …)` matching neither schema is most likely a genuine
309 // package manifest with a typo in `:nome`, and the derive's
310 // diagnostic — which names the offending keyword and suggests the
311 // nearest slot — is far better than anything this classifier
312 // could say) both return `false` from `is_molde_family()` and fall
313 // through to the derive. Only the typed dialect flows into the
314 // error — the three user-facing projections (canonical keyword,
315 // description, consumer) are read at Display time through
316 // [`crate::dialeto::CaixaDialeto`]'s own accessors, so the
317 // variant cannot carry a snapshot that drifts from
318 // [`crate::dialeto::CaixaDialeto::palavra_canonica`] /
319 // `descricao` / `consumidor`. A future fifth dialect the
320 // [`crate::dialeto`] module doc's "third dialect" hazard
321 // actualises that belongs to the `defmolde` family lands one
322 // match arm at [`crate::dialeto::CaixaDialeto::is_molde_family`]
323 // and this gate picks up the new arm by construction — the pre-
324 // lift wildcard `foreign =>` was compile-time-anonymous and would
325 // silently absorb any hypothetical fifth `defcaixa`-family arm as
326 // foreign; routing the partition through the typed predicate
327 // closes both drift surfaces.
328 let dialeto = crate::dialeto::classify_form(first)?;
329 if dialeto.is_molde_family() {
330 return Err(LeituraError::DialetoEstrangeiro { dialeto });
331 }
332
333 Self::compile_from_sexp(first).map_err(LeituraError::Leitura)
334 }
335
336 /// Register `Caixa` with the global tatara-lisp domain registry so
337 /// `defcaixa` is dispatchable from any tatara-lisp binary that seeds
338 /// the registry (e.g. `tatara-check`).
339 ///
340 /// Returns the typed [`tatara_lisp::KeywordCollision`] on the second
341 /// (and every subsequent) call in the same process — one keyword,
342 /// one type, per process is a hard invariant of the upstream
343 /// registry, and a caller that hits it must fix its crate graph
344 /// rather than swallowing the error. Peer of the sibling per-crate
345 /// `register()` entry points at `caixa-flake/src/flake.rs`,
346 /// `caixa-fmt/src/lisp_config.rs`, `caixa-lacre/src/lock.rs`,
347 /// `caixa-lint/src/lisp_config.rs`, `caixa-resolver/src/lisp_config.rs`
348 /// — every substrate crate that owns a tatara-lisp keyword now
349 /// propagates the same typed error verbatim, so a downstream binary
350 /// that seeds the registry (`tatara-check`, the future LSP) reaches
351 /// for one shape at every call site.
352 ///
353 /// # Errors
354 ///
355 /// [`tatara_lisp::KeywordCollision`] when a peer type has already
356 /// claimed the `defcaixa` keyword in this process.
357 pub fn register() -> Result<(), tatara_lisp::KeywordCollision> {
358 tatara_lisp::domain::register::<Self>()
359 }
360
361 /// Substrate-canonical per-`Caixa` `:licenca` SPDX-expression scalar
362 /// accessor every consumer of the top-level manifest's license axis
363 /// keys off — returns the author-declared `:licenca` byte-string
364 /// verbatim as an `Option<&str>`, borrowed from the typed slot's own
365 /// `Option<String>` storage. `None` when the slot is absent (the
366 /// canonical "omit to defer to the caixa-helm renderer's `MIT`
367 /// fallback" shape [`Self::validate_licenca`] documents at
368 /// caixa-core/src/manifest.rs:1560; the peer [`caixa-helm`]
369 /// `build_readme` fold at caixa-helm/src/lib.rs:962 reads this
370 /// predicate too, so an authored-but-unset `:licenca` round-trips to
371 /// a rendered `lareira-<nome>` chart's `README.md` `## License`
372 /// section structurally identical to one that omits the slot).
373 ///
374 /// The `:licenca` slot carries the universal-axis SPDX-expression
375 /// license identifier every kind of caixa emits under (CAIXA-SDLC
376 /// §I — the author-facing surface every `defcaixa` form supplies) —
377 /// the typed slot's `Option<String>` accept-set (empty-string
378 /// rejected through [`ManifestError::LicencaEmpty`], SPDX-alphabet-
379 /// invalid rejected through [`ManifestError::LicencaInvalid`]) maps
380 /// onto the `lareira-<nome>` Helm chart's `README.md` `## License`
381 /// section (caixa-helm/src/lib.rs:962) and (through future
382 /// tightening documented at [`Self::validate_licenca`]) the
383 /// Chart.yaml `annotations["artifacthub.io/license"]` axis every
384 /// registry-facing chart carries. Every downstream consumer that
385 /// reads the license byte-string keys off this scalar (the
386 /// [`Self::validate_licenca`] empty-arm + SPDX-shape gate that
387 /// routes through `self.licenca.as_deref()`, the caixa-helm
388 /// `build_readme` `unwrap_or_else(|| "MIT".into())` fold that keys
389 /// the fallback off the `Option::is_none()` arm, every future
390 /// per-`Caixa` registry-facing renderer the CAIXA-SDLC §I roadmap
391 /// acknowledges).
392 ///
393 /// Prior to this lift the `.licenca` field was accessed inline at
394 /// two production sites — [`Self::validate_licenca`]'s
395 /// `self.licenca.as_deref()` empty-and-shape gate binding and the
396 /// caixa-helm `build_readme` `caixa.licenca.clone().unwrap_or_else(||
397 /// "MIT".into())` `README.md` `## License` fold — two open-coded
398 /// field-accesses that expressed no compile-time link back to the
399 /// typed slot. A future extension of the `:licenca` axis to a
400 /// richer author surface — a per-`:licenca` structured SPDX
401 /// expression parser + license-id allowlist (the future tightening
402 /// [`Self::validate_licenca`]'s docstring acknowledges), a
403 /// per-cluster license-default overlay the M4 CR materializer
404 /// resolves per-CR (the "cluster policy pins `Apache-2.0` for every
405 /// unlisted caixa" arm), a promotion of the plain
406 /// `Option<String>` byte-string to a richer `SpdxExpression` enum
407 /// once the SPDX-expression parser lands — would have had to be
408 /// threaded through both open-coded copies in lockstep or the
409 /// validate gate and the caixa-helm emit path would silently
410 /// disagree on which license a given [`Caixa`] resolves to (an
411 /// author's `:licenca "MIT OR Apache-2.0"` would satisfy validate
412 /// while the emit path silently rendered a stale `MIT` fallback,
413 /// or vice versa). Lifting the resolution to a typed method on the
414 /// substrate primitive means every downstream consumer of the
415 /// caixa's per-`Caixa` license surface reaches for exactly one
416 /// typed dispatch — the resolver's accept-set migrates as a unit
417 /// on any future axis addition.
418 ///
419 /// First `Option<&str>`-return top-level [`Caixa`] scalar accessor —
420 /// opens the "outer [`Caixa`] `Option<&str>` scalar" projection
421 /// pattern the sibling per-`Caixa` `:descricao` / `:repositorio` /
422 /// `:edicao` future lifts fold on. Same "one typed dispatch on the
423 /// substrate primitive, thin projections at each consumer"
424 /// discipline the peer per-`:placement` [`crate::aplicacao::Placement::shard_key`]
425 /// (7cd2a28) / [`crate::aplicacao::Placement::affinity`] (74ec2d3)
426 /// / per-`:contratos` [`crate::aplicacao::WitContract::endpoint`]
427 /// (7020470) / [`crate::aplicacao::WitContract::subject`] (90de675)
428 /// / [`crate::aplicacao::WitContract::slot`] (ed22b66)
429 /// `Option<&str>`-return accessors carry on the sibling M2 / M3
430 /// typed-slot atom axes, extended here to the outer top-level
431 /// `Caixa` universal-axis surface. Named `licenca()` to match the
432 /// storage field's name; the accessor's identity maps onto the
433 /// canonical CAIXA-SDLC §I vocabulary the slot's docstring already
434 /// carries.
435 #[must_use]
436 pub fn licenca(&self) -> Option<&str> {
437 self.licenca.as_deref()
438 }
439
440 /// Substrate-canonical per-`Caixa` `:repositorio` git-repo-URL scalar
441 /// accessor every consumer of the top-level manifest's homepage /
442 /// source-of-truth axis keys off — returns the author-declared
443 /// `:repositorio` byte-string verbatim as an `Option<&str>`, borrowed
444 /// from the typed slot's own `Option<String>` storage. `None` when
445 /// the slot is absent (the canonical "omit to defer to the renderer's
446 /// per-target placeholder" shape — [`caixa-helm`]'s `ChartYaml.home`
447 /// carries the `Option<String>` through verbatim so an author-omitted
448 /// `:repositorio` renders a `Chart.yaml` without a `home:` field
449 /// (`skip_serializing_if = "Option::is_none"`), while [`caixa-flux`]'s
450 /// `ClusterBundleOpts::for_caixa` folds the omitted slot through a
451 /// `format!("https://github.com/{DEFAULT_PLEME_GIT_ORG}/{nome}")`
452 /// fallback derived from `caixa.nome`).
453 ///
454 /// The `:repositorio` slot carries the universal-axis git-repo-URL
455 /// homepage identifier every kind of caixa emits under (CAIXA-SDLC
456 /// §I — the author-facing surface every `defcaixa` form supplies) —
457 /// the typed slot's `Option<String>` accept-set (empty-string
458 /// rejected through [`ManifestError::RepositorioEmpty`], git-repo-URL-
459 /// shape-invalid rejected through [`ManifestError::RepositorioInvalid`]
460 /// past the shared [`crate::render::is_git_repo_url`] predicate the
461 /// peer per-`:deps :fonte :repo` axis also routes through) maps onto
462 /// four load-bearing downstream consumers:
463 ///
464 /// - [`Self::validate_repositorio`]'s empty-arm + shape-predicate
465 /// gate binding at caixa-core/src/manifest.rs:1456 — the
466 /// universal-axis identity gate wired at caixa-build time.
467 /// - [`caixa-helm`]'s `build_chart_yaml` `ChartYaml.home` fold at
468 /// caixa-helm/src/lib.rs:840 — the rendered `lareira-<nome>`
469 /// Helm chart's `Chart.yaml` `home:` field, which every registry
470 /// that ingests the chart (ArtifactHub, chartmuseum,
471 /// `helm search repo`) surfaces as the chart's canonical source-
472 /// of-truth link.
473 /// - [`caixa-helm`]'s `build_readme` `## Source` fold at
474 /// caixa-helm/src/lib.rs:957 — the rendered `lareira-<nome>`
475 /// chart's `README.md` header link back to the source repo,
476 /// which every author who inspects the rendered chart bundle
477 /// lands at.
478 /// - [`caixa-flux`]'s `ClusterBundleOpts::for_caixa`
479 /// `GitRepository.spec.url` fold at caixa-flux/src/lib.rs:2006 —
480 /// the rendered `GitRepository` CR's `spec.url` field, which
481 /// FluxCD's `source-controller` polls to reconcile the caixa's
482 /// manifest bundle from git.
483 ///
484 /// Prior to this lift the `.repositorio` field was accessed inline
485 /// at four production sites — [`Self::validate_repositorio`]'s
486 /// `self.repositorio.as_deref()` empty-and-shape gate binding, the
487 /// caixa-helm `build_chart_yaml` `caixa.repositorio.clone()`
488 /// `Chart.yaml` `home:` field fold, the caixa-helm `build_readme`
489 /// `caixa.repositorio.clone().unwrap_or_else(|| caixa.nome.clone())`
490 /// `README.md` `## Source` fold, and the caixa-flux
491 /// `ClusterBundleOpts::for_caixa`
492 /// `caixa.repositorio.clone().unwrap_or_else(|| format!(...))`
493 /// `GitRepository.spec.url` fold — four open-coded field-accesses
494 /// that expressed no compile-time link back to the typed slot. A
495 /// future extension of the `:repositorio` axis to a richer author
496 /// surface — a per-`:repositorio` structured
497 /// [`crate::render::GitRepoUrl`]-shaped scheme+host+path parse
498 /// (the future tightening [`Self::validate_repositorio`]'s
499 /// docstring anticipates alongside the peer per-`:deps :fonte
500 /// :repo` axis), a per-cluster repo-mirror overlay the M4 CR
501 /// materializer resolves per-CR (the "cluster policy rewrites
502 /// `github:pleme-io/...` to `git.internal/mirror/pleme-io/...`"
503 /// arm the private-registry story acknowledges), a promotion of
504 /// the plain `Option<String>` byte-string to a richer
505 /// `RepoUrl` enum discriminated on scheme — would have had to be
506 /// threaded through all four open-coded copies in lockstep or the
507 /// validate gate and the three emit paths would silently disagree
508 /// on which URL a given [`Caixa`] resolves to (an author's
509 /// `:repositorio "github:pleme-io/checkout"` would satisfy validate
510 /// while one of the emit paths silently rendered a stale URL, or
511 /// vice versa). Lifting the resolution to a typed method on the
512 /// substrate primitive means every downstream consumer of the
513 /// caixa's per-`Caixa` repo-URL surface reaches for exactly one
514 /// typed dispatch — the resolver's accept-set migrates as a unit on
515 /// any future axis addition.
516 ///
517 /// Second outer top-level [`Caixa`] `Option<&str>`-return scalar
518 /// accessor — sibling of [`Self::licenca`] (6d5bc28), the accessor
519 /// that opened the "outer [`Caixa`] `Option<&str>` scalar"
520 /// projection pattern this lift folds on. Same "one typed dispatch
521 /// on the substrate primitive, thin projections at each consumer"
522 /// discipline the peer per-`:placement`
523 /// [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
524 /// [`crate::aplicacao::Placement::affinity`] (74ec2d3) /
525 /// per-`:contratos` [`crate::aplicacao::WitContract::endpoint`]
526 /// (7020470) / [`crate::aplicacao::WitContract::subject`] (90de675)
527 /// / [`crate::aplicacao::WitContract::slot`] (ed22b66)
528 /// `Option<&str>`-return accessors carry on the sibling M2 / M3
529 /// typed-slot atom axes, extended here to the second outer top-level
530 /// `Caixa` universal-axis surface. Named `repositorio()` to match
531 /// the storage field's name; the accessor's identity maps onto the
532 /// canonical CAIXA-SDLC §I vocabulary the slot's docstring already
533 /// carries.
534 #[must_use]
535 pub fn repositorio(&self) -> Option<&str> {
536 self.repositorio.as_deref()
537 }
538
539 /// Substrate-canonical per-`Caixa` **resolved-git-repo-URL** composer —
540 /// returns the caixa's canonical git-source-of-truth URL as an owned
541 /// [`String`], author-declared `:repositorio` byte-string verbatim on
542 /// the `Some` arm and the substrate's canonical pleme-org github URL
543 /// fallback ([`crate::DEFAULT_PLEME_GIT_ORG`] and [`Self::nome`]
544 /// interpolated into `https://github.com/<org>/<nome>`) on the
545 /// `None` arm. Every substrate-side consumer that resolves
546 /// "which git URL does this caixa's source live at?" reaches for
547 /// exactly one typed dispatch on the substrate primitive — the raw
548 /// `caixa.repositorio().map(str::to_owned).unwrap_or_else(|| format!(
549 /// "https://github.com/{org}/{nome}", org = DEFAULT_PLEME_GIT_ORG,
550 /// nome = caixa.nome()))` open-coded composition every prior caller
551 /// re-derived collapses onto one canonical arm.
552 ///
553 /// Distinct from [`Self::repositorio`] (`Option<&str>`, exposes the
554 /// author-omitted / author-declared partition to the caller) — this
555 /// accessor is the **resolved** URL surface, folding the fallback in
556 /// at the substrate-primitive boundary. Every consumer that keys off
557 /// the `Option::is_none()` discriminator (a [`Chart.yaml`] `home:`
558 /// field emit that must omit the field entirely on an author-omitted
559 /// `:repositorio`, per the [`Self::repositorio`] docstring's
560 /// documented four-consumer list) reaches through the raw
561 /// [`Self::repositorio`] `Option<&str>` accessor by construction — the
562 /// resolved-URL composer sits alongside it as the second projection
563 /// on the same underlying `:repositorio` slot rather than replacing
564 /// the raw accessor.
565 ///
566 /// The fallback branch is the exact byte-image of the prior inline
567 /// [`caixa-flux::ClusterBundleOpts::for_caixa`] `git_url` composer at
568 /// caixa-flux/src/lib.rs:2080 — pinned by the sibling caixa-flux
569 /// byte-parity test
570 /// `cluster_bundle_opts_for_caixa_git_url_routes_through_canonical_git_url_accessor`
571 /// against a future implementation of this method that reordered the
572 /// `format!` template arguments, migrated the `<org>` segment to a
573 /// different constant (the [`crate::DEFAULT_PLEME_GIT_ORG`] axis a
574 /// future substrate-side git-org migration may split off), or
575 /// silently absorbed the empty-string arm (a hypothetical
576 /// `Some("") → fallback` collapse the raw [`Self::repositorio`]
577 /// accessor's docstring explicitly rejects on the sibling raw
578 /// accessor).
579 ///
580 /// Peer of the sibling per-`&Caixa`-axis composed helpers
581 /// [`caixa-flux::cluster_bundle_for_caixa`] (06d52d7) on the sibling
582 /// substrate-side renderer surface — same "close the composed
583 /// substrate-primitive at one canonical arm on the single-`&Caixa`
584 /// dispatch, converge every prior open-coded caller onto the arm"
585 /// discipline extended onto the resolved-git-URL projection of the
586 /// per-`Caixa` `:repositorio` axis. Owns per-call [`String`]
587 /// allocation on both arms (the `Some` arm's `str::to_owned` and the
588 /// `None` arm's `format!`) — the by-value return matches every
589 /// downstream consumer's field-fill shape (the caixa-flux
590 /// `ClusterBundleOpts::git_url: String` field, every future
591 /// `Chart.yaml` `home:` fold's `Option<String>` field-fill on the
592 /// `Some` arm).
593 #[must_use]
594 pub fn canonical_git_url(&self) -> String {
595 self.repositorio().map_or_else(
596 || {
597 format!(
598 "https://github.com/{org}/{nome}",
599 org = crate::DEFAULT_PLEME_GIT_ORG,
600 nome = self.nome(),
601 )
602 },
603 str::to_owned,
604 )
605 }
606
607 /// Substrate-canonical per-`Caixa` **resolved-publish-tag** composer —
608 /// returns the caixa's canonical Zig-style git-publish-tag as an owned
609 /// [`String`], derived by concatenating
610 /// [`crate::DEFAULT_PUBLISH_TAG_PREFIX`] with the typed
611 /// [`Self::versao`] byte-string on a single `format!` template.
612 /// Every substrate-side consumer that resolves "which git tag does this
613 /// caixa publish under?" reaches for exactly one typed dispatch on the
614 /// substrate primitive — the raw `format!("{prefix}{versao}", prefix =
615 /// caixa_core::DEFAULT_PUBLISH_TAG_PREFIX, versao = caixa.versao())`
616 /// open-coded composition every prior caller re-derived collapses onto
617 /// one canonical arm.
618 ///
619 /// Peer of the sibling [`Self::canonical_git_url`] (124f864) resolved-
620 /// git-URL composer on the paired per-`Caixa` git-remote axis — same
621 /// "close the composed substrate-primitive at one canonical arm on the
622 /// single-`&Caixa` dispatch, converge every prior open-coded caller
623 /// onto the arm" discipline extended from the resolved-URL projection
624 /// of the per-`Caixa` `:repositorio` axis onto the resolved-tag
625 /// projection of the per-`Caixa` `:versao` axis. The two accessors
626 /// jointly close the pair of scalars every `FluxCD` `GitRepository` CR
627 /// keys off (`spec.url` via [`Self::canonical_git_url`],
628 /// `spec.ref.tag` via [`Self::publish_tag`]) at the substrate primitive
629 /// — a downstream consumer that reaches through both accessors reads
630 /// the complete published-git-identity of a caixa through two typed
631 /// dispatches, not four open-coded field accesses.
632 ///
633 /// The reader-side (`caixa-flux::cluster_bundle` /
634 /// `ClusterBundleOpts::for_caixa`'s `git_ref` field, every future
635 /// per-cluster snapshot bundle emitter, the future M4
636 /// `mesh.pleme.io/v1alpha1/Aplicacao` CR materializer's tag-carrier
637 /// slot on the tatara `Process` intent) always resolves the tag under
638 /// the canonical [`crate::DEFAULT_PUBLISH_TAG_PREFIX`] prefix — this
639 /// method encodes that reader-side convention. The writer-side
640 /// (`caixa-feira`'s `feira publish` `--prefix` clap flag) allows the
641 /// operator to override the prefix at publish time; the two surfaces
642 /// intentionally sit on the "canonical default + operator override"
643 /// pair the sibling [`crate::DEFAULT_PUBLISH_TAG_PREFIX`] constant's
644 /// own docstring documents — a `feira publish --prefix release/`
645 /// override is the operator's explicit opt-out from the substrate
646 /// default, not a supported drift axis.
647 ///
648 /// The composition body is the exact byte-image of the prior inline
649 /// [`caixa-flux::ClusterBundleOpts::for_caixa`] `git_ref` composer at
650 /// caixa-flux/src/lib.rs:2105 — pinned by the sibling caixa-flux
651 /// byte-parity test
652 /// `cluster_bundle_opts_for_caixa_git_ref_routes_through_publish_tag_accessor`
653 /// against a future implementation of this method that reordered the
654 /// `format!` template arguments, migrated the `<prefix>` segment to a
655 /// different constant (the [`crate::DEFAULT_PUBLISH_TAG_PREFIX`] axis
656 /// a future Zig-style-tag rebrand may split off — the constant's own
657 /// docstring anticipates a substrate-side move to `release/<versao>`
658 /// or bare `<versao>` shapes once a sibling forge convention adopts a
659 /// slash-namespaced or bare-scalar form), interposed a canonicalization
660 /// pass on the `:versao` axis (a SemVer-2 build-metadata strip an OCI-
661 /// tag normalizer might apply once the M4 registry-alignment slot
662 /// lands), or silently absorbed an empty `:versao` arm (which cannot
663 /// occur past the [`Self::validate_versao`] gate but which a
664 /// hypothetical bypass on the accessor path must not silently paper
665 /// over).
666 ///
667 /// Owns per-call [`String`] allocation via the single `format!`
668 /// invocation — the by-value return matches every downstream
669 /// consumer's field-fill shape (the caixa-flux `GitRefSpec::Tag(String)`
670 /// variant's owned payload, every future `intent.aplicacao.tag: String`
671 /// field-fill on the M4 CR materializer's tag-carrier slot).
672 #[must_use]
673 pub fn publish_tag(&self) -> String {
674 format!(
675 "{prefix}{versao}",
676 prefix = crate::DEFAULT_PUBLISH_TAG_PREFIX,
677 versao = self.versao(),
678 )
679 }
680
681 /// Substrate-canonical per-`Caixa` **resolved-Helm-chart-name** composer
682 /// — returns the caixa's canonical `lareira-<nome>` per-Servico Helm
683 /// chart identity as an owned [`String`], derived by dispatching through
684 /// the substrate-canonical [`crate::lareira_chart_name`] helper against
685 /// the typed [`Self::nome`] byte-string. Every substrate-side consumer
686 /// that resolves "which Helm chart identity does this caixa render
687 /// under?" reaches for exactly one typed dispatch on the substrate
688 /// primitive — the raw `caixa_core::lareira_chart_name(caixa.nome())`
689 /// two-step compose every prior caller re-derived collapses onto one
690 /// canonical arm on the single-`&Caixa` dispatch.
691 ///
692 /// Peer of the sibling [`Self::canonical_git_url`] (124f864) resolved-
693 /// git-URL composer + [`Self::publish_tag`] (07e05b8) resolved-publish-
694 /// tag composer on the paired per-`Caixa` published-artifact-identity
695 /// axis — same "close the composed substrate-primitive at one canonical
696 /// arm on the single-`&Caixa` dispatch, converge every prior open-coded
697 /// caller onto the arm" discipline extended from the resolved-URL /
698 /// resolved-tag projections of the `:repositorio` / `:versao` axes onto
699 /// the resolved-chart-name projection of the `:nome` axis. The three
700 /// accessors jointly close the triple of scalars every per-Servico
701 /// deploy artifact keys off (git source URL via
702 /// [`Self::canonical_git_url`], git source tag via
703 /// [`Self::publish_tag`], per-Servico Helm chart identity via
704 /// [`Self::lareira_chart_name`]) at the substrate primitive — a
705 /// downstream consumer that reaches through all three reads the
706 /// complete deploy-artifact identity of a caixa through three typed
707 /// dispatches, not six open-coded compositions across three renderer
708 /// crates.
709 ///
710 /// The reader-side (three production sites at the time of the lift —
711 /// [`caixa-helm::render_chart_for_servico_with`]'s `ChartDir.name`
712 /// composer at caixa-helm/src/lib.rs:778, the peer
713 /// [`caixa-flux::cluster_bundle`]'s per-CR `chart_name` binding at
714 /// caixa-flux/src/lib.rs:2219, and
715 /// [`caixa-tatara::process_for_aplicacao`]'s `release_name`
716 /// composer at caixa-tatara/src/lib.rs:227, plus every future
717 /// per-Servico OCI publish emitter the CAIXA-SDLC §II
718 /// `caixa-publish.yml` reusable workflow's `skopeo push` step keys
719 /// off, the future per-cluster snapshot bundle emitter, the future
720 /// M4 `mesh.pleme.io/v1alpha1/Aplicacao` CR materializer's
721 /// per-member chart-carrier slot on the tatara `Process` intent) —
722 /// always resolves the chart name under the canonical
723 /// [`crate::LAREIRA_CHART_NAME_PREFIX`] prefix; this method encodes
724 /// that reader-side convention. The joint-length invariant the peer
725 /// [`Self::validate_nome_chart_name_budget`] gate enforces at
726 /// caixa-build time (author-declared `:nome` + fixed prefix ≤
727 /// [`crate::DNS_1123_LABEL_MAX_LEN`]) is verified on the input to
728 /// this composer by construction, so the produced `lareira-<nome>`
729 /// string is a valid Helm chart-name segment on every accept-set
730 /// input.
731 ///
732 /// The composition body is the exact byte-image of the prior inline
733 /// `caixa_core::lareira_chart_name(caixa.nome())` two-step form every
734 /// prior caller re-derived — pinned by the sibling caixa-helm /
735 /// caixa-flux / caixa-tatara byte-parity tests
736 /// `<crate>_lareira_chart_name_routes_through_caixa_accessor` against
737 /// a future implementation of this method that reordered the
738 /// composition arguments, migrated the `<prefix>` segment to a
739 /// different constant (the [`crate::LAREIRA_CHART_NAME_PREFIX`] axis a
740 /// future substrate-side chart-family rebrand may split off — the
741 /// constant's own docstring anticipates a substrate-side move once
742 /// the `lareira-` scoping intent outlives the family it names),
743 /// interposed a canonicalization pass on the `:nome` axis (a per-
744 /// registry namespace-qualification an M4 CR materializer might apply
745 /// per-CR — the "`pleme-io/checkout` vs `partner-org/checkout`
746 /// collision" arm the multi-tenant-registry story acknowledges), or
747 /// silently absorbed an empty `:nome` arm (which cannot occur past
748 /// the [`Self::validate_nome`] gate but which a hypothetical bypass
749 /// on the accessor path must not silently paper over).
750 ///
751 /// Owns per-call [`String`] allocation via the single
752 /// [`crate::lareira_chart_name`] `format!` invocation — the by-value
753 /// return matches every downstream consumer's field-fill shape (the
754 /// caixa-helm `ChartDir.name: String` field, the caixa-flux per-CR
755 /// `chart_name: String` binding, the caixa-tatara
756 /// `AplicacaoIntent.release_name: Option<String>` field-fill on the
757 /// `Some` arm).
758 #[must_use]
759 pub fn lareira_chart_name(&self) -> String {
760 crate::lareira_chart_name(self.nome())
761 }
762
763 /// Substrate-canonical per-`Caixa` **resolved-OCI-chart-ref** composer
764 /// — returns the caixa's canonical `oci://<registry>/lareira-<nome>`
765 /// per-Servico Helm chart OCI artifact reference as an owned
766 /// [`String`], derived by dispatching through the substrate-canonical
767 /// [`crate::oci_chart_ref`] helper (which itself composes
768 /// [`crate::OCI_SCHEME_PREFIX`] + the caller-supplied `registry` +
769 /// [`crate::lareira_chart_name`]-of-[`Self::nome`]) against the
770 /// caller-supplied `registry` and the typed [`Self::nome`] byte-string.
771 /// Every substrate-side consumer that resolves "which OCI chart
772 /// artifact does this caixa publish under, in this registry?" reaches
773 /// for exactly one typed dispatch on the substrate primitive — the raw
774 /// `caixa_core::oci_chart_ref(registry, caixa.nome())` two-step compose
775 /// every prior caller re-derived collapses onto one canonical arm on
776 /// the single-`(&Caixa, &str)` dispatch.
777 ///
778 /// Fourth member of the paired per-`Caixa` published-artifact-identity
779 /// axis alongside [`Self::canonical_git_url`] (124f864) /
780 /// [`Self::publish_tag`] (07e05b8) / [`Self::lareira_chart_name`]
781 /// (a8f0bee) — same "close the composed substrate-primitive at one
782 /// canonical arm on the single-`&Caixa` dispatch, converge every
783 /// prior open-coded caller onto the arm" discipline extended from the
784 /// resolved-URL / resolved-tag / resolved-chart-name projections of
785 /// the `:repositorio` / `:versao` / `:nome` axes onto the resolved-
786 /// OCI-ref projection over the paired `(registry, :nome)` inputs. The
787 /// four accessors jointly close the per-`Caixa` published-artifact-
788 /// identity surface every downstream consumer of a caixa's published
789 /// deploy artifacts keys off (git source URL via
790 /// [`Self::canonical_git_url`], git source tag via
791 /// [`Self::publish_tag`], per-Servico Helm chart identity via
792 /// [`Self::lareira_chart_name`], per-registry OCI chart artifact
793 /// reference via [`Self::oci_chart_ref`]) at the substrate primitive
794 /// — a downstream consumer that reaches through all four reads the
795 /// complete deploy-artifact identity of a caixa through four typed
796 /// dispatches, not eight open-coded compositions across four renderer
797 /// crates. The unique-signature dispatch (`(&Caixa, &str)` on this
798 /// method vs. `&Caixa` on the sibling three) reflects the extra input
799 /// axis this composer folds in: unlike the git-URL / git-tag / chart-
800 /// name axes (each derived purely from a `&Caixa`), the OCI-ref axis
801 /// pairs the caixa's per-`:nome` chart identity with the caller-
802 /// supplied per-registry authority segment, so the accessor threads
803 /// the registry byte-string through as a positional `&str`.
804 ///
805 /// The reader-side (one production site at the time of the lift —
806 /// [`caixa-tatara::process_for_aplicacao`]'s `derive_chart_ref` helper
807 /// at caixa-tatara/src/lib.rs:333 that composes the emitted
808 /// `AplicacaoIntent.chart_ref` scalar the tatara-reconciler feeds into
809 /// `helm install`, plus every future per-Servico OCI publish emitter
810 /// the CAIXA-SDLC §II `caixa-publish.yml` reusable workflow's
811 /// `skopeo push` step keys off, the future per-cluster snapshot bundle
812 /// emitter's per-CR `oci://…` field-fill on the M4 registry-alignment
813 /// slot, the future M4 `mesh.pleme.io/v1alpha1/Aplicacao` CR
814 /// materializer's per-member `chart_ref` slot on the tatara `Process`
815 /// intent, the `FluxCD` `HelmRelease` `spec.chart.spec.chart` field-fill
816 /// on the OCI-source path an M4 per-cluster registry-rewrite overlay
817 /// applies per-CR) — always resolves the OCI ref under the canonical
818 /// [`crate::OCI_SCHEME_PREFIX`] scheme prefix + the canonical
819 /// [`Self::lareira_chart_name`] chart-name segment; this method
820 /// encodes that reader-side convention.
821 ///
822 /// The composition body is the exact byte-image of the prior inline
823 /// `caixa_core::oci_chart_ref(registry, caixa.nome())` two-step form
824 /// every prior caller re-derived — pinned by the sibling caixa-tatara
825 /// byte-parity test
826 /// `derive_chart_ref_routes_through_caixa_oci_chart_ref_accessor`
827 /// against a future implementation of this method that reordered the
828 /// composition arguments, migrated the `<scheme>` segment to a
829 /// different constant (the [`crate::OCI_SCHEME_PREFIX`] axis a future
830 /// substrate-side registry-protocol rebrand may split off — the
831 /// constant's own docstring anticipates a substrate-side move once
832 /// Helm 3 / `FluxCD` introduce a successor scheme past `oci://`),
833 /// migrated the `<chart>` segment off the paired
834 /// [`crate::lareira_chart_name`] composer (a per-registry
835 /// namespace-qualification an M4 CR materializer might apply per-CR),
836 /// interposed a canonicalization pass on the `registry` axis (an OCI-
837 /// authority normalization once the M4 registry-alignment slot lands),
838 /// or silently absorbed an empty `:nome` arm (which cannot occur past
839 /// the [`Self::validate_nome`] gate but which a hypothetical bypass
840 /// on the accessor path must not silently paper over).
841 ///
842 /// Owns per-call [`String`] allocation via the single
843 /// [`crate::oci_chart_ref`] `format!` invocation — the by-value return
844 /// matches every downstream consumer's field-fill shape (the caixa-
845 /// tatara `AplicacaoIntent.chart_ref: String` field-fill, every
846 /// future `intent.aplicacao.chart_ref: String` field-fill on the M4
847 /// CR materializer's chart-ref-carrier slot, every future
848 /// `HelmRelease.spec.chart.spec.chart: String` field-fill on the OCI-
849 /// source path).
850 #[must_use]
851 pub fn oci_chart_ref(&self, registry: &str) -> String {
852 crate::oci_chart_ref(registry, self.nome())
853 }
854
855 /// Substrate-canonical per-`Caixa` `:descricao` free-form-prose
856 /// chart-description scalar accessor every consumer of the top-level
857 /// manifest's Chart.yaml `description:` axis keys off — returns the
858 /// author-declared `:descricao` byte-string verbatim as an
859 /// `Option<&str>`, borrowed from the typed slot's own
860 /// `Option<String>` storage. `None` when the slot is absent (the
861 /// canonical "omit to defer to the per-renderer `caixa.nome`-derived
862 /// fallback" shape — [`caixa-helm`]'s `build_chart_yaml` folds the
863 /// omitted slot through a `format!("Generated chart for caixa Servico
864 /// {}", caixa.nome)` fallback, [`caixa-helm`]'s `build_readme` folds
865 /// it through a `format!("caixa Servico {}", caixa.nome)` fallback,
866 /// and [`caixa-feira`]'s `render_flake` folds it through a
867 /// `format!("caixa {}", c.nome)` `flake.nix` `description = ""`
868 /// fallback — each derived from `caixa.nome` on the null-carrier arm).
869 ///
870 /// The `:descricao` slot carries the universal-axis free-form-prose
871 /// chart-description identifier every kind of caixa emits under
872 /// (CAIXA-SDLC §I — the author-facing surface every `defcaixa` form
873 /// supplies) — the typed slot's `Option<String>` accept-set
874 /// (empty-string rejected through [`ManifestError::DescricaoEmpty`],
875 /// chart-description-shape-invalid rejected through
876 /// [`ManifestError::DescricaoInvalid`] past the shared
877 /// [`crate::render::is_chart_description_shape`] predicate the peer
878 /// per-`Caixa` `:descricao` axis also routes through) maps onto four
879 /// load-bearing downstream consumers:
880 ///
881 /// - [`Self::validate_descricao`]'s empty-arm + shape-predicate
882 /// gate binding — the universal-axis identity gate wired at
883 /// caixa-build time.
884 /// - [`caixa-helm`]'s `build_chart_yaml` `ChartYaml.description`
885 /// `Chart.yaml` field fold — the rendered `lareira-<nome>` Helm
886 /// chart's `Chart.yaml` `description:` field, which
887 /// `apiVersion: v2` charts require non-empty (`helm lint` fires
888 /// `WARNING [chart.metadata.description]: description is required`
889 /// when absent) and which every registry that ingests the chart
890 /// (ArtifactHub, chartmuseum, `helm search repo`) surfaces as the
891 /// chart's canonical one-line prose descriptor.
892 /// - [`caixa-helm`]'s `build_readme` chart-`README.md` header fold
893 /// — the rendered `lareira-<nome>` chart's `README.md` prose
894 /// header directly beneath the `# <chart-name>` title, which
895 /// every author who inspects the rendered chart bundle lands at.
896 /// - [`caixa-feira`]'s `render_flake` `flake.nix` `description = ""`
897 /// top-level fold — the emitted `flake.nix`'s `description`
898 /// field, which every Nix consumer (`nix flake show`,
899 /// `nix flake metadata`, downstream flake-registry ingestors)
900 /// surfaces as the flake's canonical descriptor.
901 ///
902 /// Prior to this lift the `.descricao` field was accessed inline at
903 /// four production sites — [`Self::validate_descricao`]'s
904 /// `self.descricao.as_deref()` empty-and-shape gate binding, the
905 /// caixa-helm `build_chart_yaml`
906 /// `caixa.descricao.clone().unwrap_or_else(|| format!(...))`
907 /// `Chart.yaml` `description:` fold, the caixa-helm `build_readme`
908 /// `caixa.descricao.clone().unwrap_or_else(|| format!(...))`
909 /// `README.md` header fold, and the caixa-feira `render_flake`
910 /// `c.descricao.clone().unwrap_or_else(|| format!(...))` `flake.nix`
911 /// `description = ""` fold — four open-coded field-accesses that
912 /// expressed no compile-time link back to the typed slot. A future
913 /// extension of the `:descricao` axis to a richer author surface —
914 /// a per-`:descricao` locale-tagged multi-language descriptor map
915 /// (the "one caixa, N language-tagged prose descriptions" arm
916 /// author-tooling internationalization anticipates), a
917 /// per-registry-target length-and-shape overlay the M4 CR
918 /// materializer resolves per-CR (the "ArtifactHub caps description
919 /// at 512 bytes but the internal registry caps at 256" arm), a
920 /// promotion of the plain `Option<String>` byte-string to a richer
921 /// `ChartDescription` newtype guaranteeing the
922 /// `is_chart_description_shape` predicate at the type level — would
923 /// have had to be threaded through all four open-coded copies in
924 /// lockstep or the validate gate and the three emit paths would
925 /// silently disagree on which prose string a given [`Caixa`]
926 /// resolves to (an author's
927 /// `:descricao "Checkout flow orchestration."` would satisfy
928 /// validate while one of the emit paths silently rendered a stale
929 /// `caixa.nome`-derived fallback, or vice versa). Lifting the
930 /// resolution to a typed method on the substrate primitive means
931 /// every downstream consumer of the caixa's per-`Caixa`
932 /// chart-description surface reaches for exactly one typed dispatch
933 /// — the resolver's accept-set migrates as a unit on any future
934 /// axis addition.
935 ///
936 /// Third outer top-level [`Caixa`] `Option<&str>`-return scalar
937 /// accessor — sibling of [`Self::licenca`] (6d5bc28) and
938 /// [`Self::repositorio`] (cc7332d), the accessors that opened the
939 /// "outer [`Caixa`] `Option<&str>` scalar" projection pattern this
940 /// lift folds on. Same "one typed dispatch on the substrate
941 /// primitive, thin projections at each consumer" discipline the
942 /// peer per-`:placement`
943 /// [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
944 /// [`crate::aplicacao::Placement::affinity`] (74ec2d3) /
945 /// per-`:contratos` [`crate::aplicacao::WitContract::endpoint`]
946 /// (7020470) / [`crate::aplicacao::WitContract::subject`] (90de675)
947 /// / [`crate::aplicacao::WitContract::slot`] (ed22b66)
948 /// `Option<&str>`-return accessors carry on the sibling M2 / M3
949 /// typed-slot atom axes, extended here to the third outer top-level
950 /// `Caixa` universal-axis surface. Named `descricao()` to match the
951 /// storage field's name; the accessor's identity maps onto the
952 /// canonical CAIXA-SDLC §I vocabulary the slot's docstring already
953 /// carries. The one remaining universal `Option<String>` slot
954 /// (`:edicao`) folds on this pattern next.
955 #[must_use]
956 pub fn descricao(&self) -> Option<&str> {
957 self.descricao.as_deref()
958 }
959
960 /// Substrate-canonical per-`Caixa` `:edicao` language-edition scalar
961 /// accessor every consumer of the top-level manifest's tatara-lisp
962 /// edition-selector axis keys off — returns the author-declared
963 /// `:edicao` byte-string verbatim as an `Option<&str>`, borrowed from
964 /// the typed slot's own `Option<String>` storage. `None` when the
965 /// slot is absent (the canonical "omit the slot to defer to the
966 /// substrate's default edition" shape every existing
967 /// [`caixa-resolver`] integration test fixture carries via
968 /// `edicao: None` — see `caixa-resolver/tests/git_integration.rs`;
969 /// the peer [`Self::validate_edicao`] gate is a no-op on the omitted
970 /// arm by construction, so an author-omitted `:edicao` round-trips
971 /// to a build without triggering the year-shape predicate).
972 ///
973 /// The `:edicao` slot carries the universal-axis 4-digit-ASCII-
974 /// decimal-year language-edition identifier every kind of caixa
975 /// emits under (CAIXA-SDLC §I — the author-facing surface every
976 /// `defcaixa` form supplies) — the typed slot's `Option<String>`
977 /// accept-set (empty-string rejected through
978 /// [`ManifestError::EdicaoEmpty`], year-shape-invalid rejected
979 /// through [`ManifestError::EdicaoInvalid`] past the 4-digit-ASCII-
980 /// decimal-year predicate [`Self::validate_edicao`] enforces) maps
981 /// onto one load-bearing downstream consumer today
982 /// ([`Self::validate_edicao`]'s empty-arm + year-shape-predicate
983 /// gate binding at caixa-core/src/manifest.rs:1959) plus every
984 /// future edition-aware substrate consumer the CAIXA-SDLC §I
985 /// roadmap anticipates (the tatara-lisp compiler's macro-surface
986 /// selector every edition-aware build step keys off, the future
987 /// per-edition compatibility-flag overlay the M4 CR materializer
988 /// resolves per-CR, the peer [`Caixa::template`] canonical
989 /// `:edicao "2026"` scaffold every `feira init` emits verbatim,
990 /// and the renderer-side fixtures at `caixa-helm/src/lib.rs:978` /
991 /// `caixa-flux/src/lib.rs:2319` / `caixa-mesh/src/lib.rs:3208` that
992 /// carry `edicao: Some("2026".into())` by construction).
993 ///
994 /// Prior to this lift the `.edicao` field was accessed inline at
995 /// one production site — [`Self::validate_edicao`]'s
996 /// `self.edicao.as_deref()` empty-and-shape gate binding — one
997 /// open-coded field-access that expressed no compile-time link
998 /// back to the typed slot. A future extension of the `:edicao`
999 /// axis to a richer author surface — a per-`:edicao` known-
1000 /// edition allowlist (the future tightening
1001 /// [`Self::validate_edicao`]'s docstring acknowledges past the
1002 /// structural year-shape floor, rejecting year-shaped values that
1003 /// don't name a tatara-lisp edition the substrate actually
1004 /// understands — `"1999"` is year-shaped but no `1999` edition
1005 /// exists), a per-edition compatibility-flag overlay the M4 CR
1006 /// materializer resolves per-CR (the "edition `"2026"` enables
1007 /// macro-surface features the sibling `"2018"` gates behind a
1008 /// feature flag" arm the edition-selector story anticipates), a
1009 /// promotion of the plain `Option<String>` byte-string to a
1010 /// richer `CaixaEdition` enum discriminated on year once a sibling
1011 /// edition to `"2026"` lands — would have had to be threaded
1012 /// through the open-coded copy in lockstep with every future
1013 /// edition-aware consumer, or the validate gate and the future
1014 /// edition-aware consumer path would silently disagree on which
1015 /// edition a given [`Caixa`] resolves to (an author's
1016 /// `:edicao "2026"` would satisfy validate while a future
1017 /// edition-aware consumer silently defaulted to a stale edition,
1018 /// or vice versa). Lifting the resolution to a typed method on
1019 /// the substrate primitive means every downstream consumer of the
1020 /// caixa's per-`Caixa` edition surface reaches for exactly one
1021 /// typed dispatch — the resolver's accept-set migrates as a unit
1022 /// on any future axis addition.
1023 ///
1024 /// Fourth and final outer top-level [`Caixa`] `Option<&str>`-return
1025 /// scalar accessor — sibling of [`Self::licenca`] (6d5bc28),
1026 /// [`Self::repositorio`] (cc7332d), and [`Self::descricao`]
1027 /// (3f16e2f), the accessors that opened the "outer [`Caixa`]
1028 /// `Option<&str>` scalar" projection pattern this lift folds on.
1029 /// Same "one typed dispatch on the substrate primitive, thin
1030 /// projections at each consumer" discipline the peer per-`:placement`
1031 /// [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
1032 /// [`crate::aplicacao::Placement::affinity`] (74ec2d3) /
1033 /// per-`:contratos` [`crate::aplicacao::WitContract::endpoint`]
1034 /// (7020470) / [`crate::aplicacao::WitContract::subject`] (90de675)
1035 /// / [`crate::aplicacao::WitContract::slot`] (ed22b66)
1036 /// `Option<&str>`-return accessors carry on the sibling M2 / M3
1037 /// typed-slot atom axes, extended here to close the outer top-level
1038 /// `Caixa` universal-axis surface's last unlifted `Option<String>`
1039 /// slot. Named `edicao()` to match the storage field's name; the
1040 /// accessor's identity maps onto the canonical CAIXA-SDLC §I
1041 /// vocabulary the slot's docstring already carries.
1042 #[must_use]
1043 pub fn edicao(&self) -> Option<&str> {
1044 self.edicao.as_deref()
1045 }
1046
1047 /// Substrate-canonical per-`Caixa` `:nome` universal-axis DNS-1123-
1048 /// label caixa-identity scalar accessor every consumer of the top-
1049 /// level manifest's identity axis keys off — returns the author-
1050 /// declared `:nome` byte-string verbatim as an `&str`, borrowed from
1051 /// the typed slot's own `String` storage. Non-optional (`:nome` is
1052 /// a required-axis scalar every `defcaixa` form must supply; the
1053 /// [`Self::from_lisp`] derive rejects an omitted / non-string
1054 /// `:nome` at parse time, so a `Caixa` past parse definitionally
1055 /// carries a non-`None` `:nome`).
1056 ///
1057 /// The `:nome` slot carries the universal-axis DNS-1123-label
1058 /// caixa-identity every kind of caixa emits under (CAIXA-SDLC §I —
1059 /// the primary identity axis every `defcaixa` form supplies
1060 /// alongside `:versao` / `:kind`; the substrate-wide identity every
1061 /// other typed surface that names a caixa reaches through — `:deps`
1062 /// entries, `:membros` entries, `:children` entries, the
1063 /// `lareira-<nome>` Helm chart name every per-Servico renderer
1064 /// derives, the `pleme-program-<nome>` label every per-Aplicacao
1065 /// renderer emits) — the typed slot's `String` accept-set (empty
1066 /// rejected through [`ManifestError::NomeEmpty`], DNS-1123-shape-
1067 /// invalid rejected through [`ManifestError::NomeInvalid`] past
1068 /// the shared [`crate::render::require_valid_dns_1123_label`] gate
1069 /// the peer name axes each land on, joint-length-with-`lareira-`-
1070 /// prefix rejected through
1071 /// [`ManifestError::NomeChartNameBudgetExceeded`] past
1072 /// [`crate::render::is_lareira_chart_name_shape`]) maps onto every
1073 /// load-bearing downstream consumer the substrate carries — the
1074 /// two universal-axis validate gates at caixa-build time
1075 /// ([`Self::validate_nome`] + [`Self::validate_nome_chart_name_budget`]),
1076 /// [`crate::lareira_chart_name`]'s `lareira-<nome>` Helm chart-name
1077 /// derivation every per-Servico renderer keys off, the caixa-helm
1078 /// `Chart.yaml`'s `name:` axis, caixa-flux's `programs.yaml` entry
1079 /// `name:` axis, caixa-mesh's Cilium `CiliumNetworkPolicy` /
1080 /// `HTTPRoute` per-Aplicacao name axes at
1081 /// caixa-mesh/src/lib.rs:{2650, 2797, 2919, 2925},
1082 /// [`crate::pleme_program_selector`] /
1083 /// [`crate::pleme_program_in_aplicacao_selector`] label-selector
1084 /// derivations, and every future substrate renderer that emits an
1085 /// artifact keyed by the caixa's identity.
1086 ///
1087 /// Prior to this lift the `.nome` field was accessed inline at a
1088 /// dozen production sites across `caixa-core` (the two universal-
1089 /// axis validate gates + [`Dep::validate`]-adjacent duplicate
1090 /// tracking), `caixa-helm` (the `lareira_chart_name` fold, the
1091 /// `ChartYaml.name` / `ChartYaml.description` / `Chart.yaml`
1092 /// `keywords` fallback), `caixa-flux` (the `programs.yaml`
1093 /// entry `name:` fold, the `flux_kustomization_source_subtree`
1094 /// per-cluster subpath derivation), and `caixa-mesh` (the
1095 /// `pleme_program_in_aplicacao_selector` label-selector fold, the
1096 /// `cilium_network_policy_name` / `gateway_api_http_route_name`
1097 /// per-CR name derivations, the `LABEL_APLICACAO` labels-map
1098 /// insert) — a dozen open-coded field-accesses that expressed no
1099 /// compile-time link back to the typed slot. A future extension of
1100 /// the `:nome` axis to a richer author surface — a per-`:nome`
1101 /// structured `CaixaIdentity` newtype that carries the joint-
1102 /// length-with-prefix invariant [`Self::validate_nome_chart_name_budget`]
1103 /// enforces at the type level (rather than as a validate-time
1104 /// gate), a per-registry `:nome` namespacing overlay the M4 CR
1105 /// materializer resolves per-CR (the "`pleme-io/checkout` vs
1106 /// `partner-org/checkout` collision" arm the multi-tenant-registry
1107 /// story acknowledges), a promotion of the plain `String` byte-
1108 /// string to a richer `CaixaNome` newtype discriminated on
1109 /// namespace prefix — would have had to be threaded through every
1110 /// open-coded copy in lockstep or the two validate gates and the
1111 /// dozen emit paths would silently disagree on which identity a
1112 /// given [`Caixa`] resolves to (an author's `:nome "checkout"`
1113 /// would satisfy validate while one of the emit paths silently
1114 /// rendered a drifted other identity, or vice versa). Lifting the
1115 /// resolution to a typed method on the substrate primitive means
1116 /// every downstream consumer of the caixa's per-`Caixa` identity
1117 /// surface reaches for exactly one typed dispatch — the resolver's
1118 /// accept-set migrates as a unit on any future axis addition.
1119 ///
1120 /// First outer top-level [`Caixa`] `&str`-return required-scalar
1121 /// accessor — opens the "outer [`Caixa`] `&str` required-scalar"
1122 /// projection pattern the sibling per-`Caixa` `:versao` future lift
1123 /// folds on. Sibling in shape to the peer per-`:membros`
1124 /// [`crate::aplicacao::Membro::nome`] (4a32abf) / per-`:contratos`
1125 /// [`crate::aplicacao::WitContract::source`] /
1126 /// [`crate::aplicacao::WitContract::destination`] (7f0fd43),
1127 /// [`crate::aplicacao::WitContract::world_ref`] (0804823),
1128 /// [`crate::aplicacao::Membro::versao_requirement`] (a40b0e3),
1129 /// [`crate::aplicacao::Entrada::destination`] (6db982c),
1130 /// [`crate::aplicacao::CircuitBreaker::max_failures`] (3a74062),
1131 /// per-sub-struct required-axis accessors carry on the sibling M3
1132 /// mesh-slot-atom scalar-value axes, extended here to open the
1133 /// outer top-level [`Caixa`] `&str`-return required-scalar surface.
1134 /// Named `nome()` to match the storage field's name; the accessor's
1135 /// identity maps onto the canonical CAIXA-SDLC §I vocabulary the
1136 /// slot's docstring already carries.
1137 #[must_use]
1138 pub fn nome(&self) -> &str {
1139 &self.nome
1140 }
1141
1142 /// Substrate-canonical per-`Caixa` `:versao` universal-axis SemVer-2
1143 /// pinned-version scalar accessor every consumer of the top-level
1144 /// manifest's version axis keys off — returns the author-declared
1145 /// `:versao` byte-string verbatim as an `&str`, borrowed from the
1146 /// typed slot's own `String` storage. Non-optional (`:versao` is a
1147 /// required-axis scalar every `defcaixa` form must supply alongside
1148 /// `:nome` / `:kind`; the [`Self::from_lisp`] derive rejects an
1149 /// omitted / non-string `:versao` at parse time, so a `Caixa` past
1150 /// parse definitionally carries a non-`None` `:versao`).
1151 ///
1152 /// The `:versao` slot carries the universal-axis SemVer-2
1153 /// concrete-version body every kind of caixa emits under
1154 /// (CAIXA-SDLC §I — the required-scalar every `defcaixa` form
1155 /// supplies alongside `:nome` / `:kind`; the substrate-wide
1156 /// pinned-version every downstream artifact-emitting consumer
1157 /// composes under — the `lareira-<nome>` Helm chart's `Chart.yaml`
1158 /// `version:` + `appVersion:` axes, the `feira publish` Zig-style
1159 /// `v<versao>` git tag the [`crate::DEFAULT_PUBLISH_TAG_PREFIX`]
1160 /// prefix composes on top of, the programs.yaml entry's `versao:`
1161 /// value the `lareira-fleet-programs` aggregator carries onto each
1162 /// rendered `ComputeUnit`, the OCI image's `:v<versao>` / `:latest`
1163 /// tags every substrate-side `skopeo push` writes, the lacre
1164 /// closure's pinned `concrete_versao`, and the `:upgrade-from :from`
1165 /// prior-version references peers in the exact same SemVer-2 shape).
1166 /// The typed slot's `String` accept-set (empty rejected through
1167 /// [`ManifestError::VersaoEmpty`], SemVer-2-shape-invalid rejected
1168 /// through [`ManifestError::VersaoInvalid`] past
1169 /// [`semver::Version::parse`]) maps onto every load-bearing
1170 /// downstream consumer the substrate carries — the [`Self::validate_versao`]
1171 /// universal-axis validate gate at caixa-build time, the
1172 /// [`crate::CaixaVersion::parse`] typed-wrapper resolver,
1173 /// [`caixa-helm`]'s `Chart.yaml` `version:` / `appVersion:` fold,
1174 /// [`caixa-flux`]'s `programs.yaml` entry `versao:` fold + the
1175 /// `cluster_bundle` `GitRepository` `ref: { tag: v<versao> }`
1176 /// derivation, [`caixa-mesh`]'s per-Aplicacao `programs.yaml` fan-
1177 /// out entry `versao:` fold, [`caixa-feira`]'s `feira publish` git-
1178 /// tag derivation (`format!("{prefix}{versao}")`), and every future
1179 /// substrate renderer that emits an artifact keyed by the caixa's
1180 /// pinned version.
1181 ///
1182 /// Prior to this lift the `.versao` field was accessed inline at a
1183 /// dozen production sites across `caixa-core` (the universal-axis
1184 /// [`Self::validate_versao`] gate + [`Dep::validate`]-adjacent
1185 /// version-shape gates), `caixa-helm` (the `ChartYaml.version` /
1186 /// `ChartYaml.app_version` folds), `caixa-flux` (the `programs.yaml`
1187 /// entry `versao:` fold, the `cluster_bundle` `GitRepository` `ref:
1188 /// { tag: v<versao> }` derivation), `caixa-mesh` (the per-Aplicacao
1189 /// `programs.yaml` fan-out entry `versao:` fold), and `caixa-feira`
1190 /// (the `feira publish` git-tag derivation + the `feira app graph` /
1191 /// `feira app deploy` diagnostic renderers) — a dozen open-coded
1192 /// field-accesses that expressed no compile-time link back to the
1193 /// typed slot. A future extension of the `:versao` axis to a richer
1194 /// author surface — a per-`:versao` structured `CaixaVersion` at the
1195 /// storage layer (the substrate already carries a `CaixaVersion`
1196 /// newtype at [`crate::version::CaixaVersion`], deferred until the
1197 /// serde-transparent-newtype-through-DeriveTataraDomain path lands),
1198 /// a per-registry `:versao` immutability overlay the M4 CR
1199 /// materializer enforces per-CR, a promotion of the plain `String`
1200 /// byte-string to a richer `PinnedVersao` newtype discriminated on
1201 /// SemVer-2 pre-release / build-metadata presence — would have had
1202 /// to be threaded through every open-coded copy in lockstep or the
1203 /// validate gate and the dozen emit paths would silently disagree
1204 /// on which version a given [`Caixa`] resolves to (an author's
1205 /// `:versao "0.1.0"` would satisfy validate while one of the emit
1206 /// paths silently rendered a drifted other version, or vice versa).
1207 /// Lifting the resolution to a typed method on the substrate
1208 /// primitive means every downstream consumer of the caixa's
1209 /// per-`Caixa` pinned-version surface reaches for exactly one typed
1210 /// dispatch — the resolver's accept-set migrates as a unit on any
1211 /// future axis addition.
1212 ///
1213 /// Second outer top-level [`Caixa`] `&str`-return required-scalar
1214 /// accessor — folds on the "outer [`Caixa`] `&str` required-scalar"
1215 /// projection pattern the sibling per-`Caixa` [`Self::nome`]
1216 /// (e6b7d97) opened. Sibling in shape to the peer per-`:membros`
1217 /// [`crate::aplicacao::Membro::versao_requirement`] (4127bb6) /
1218 /// per-`:children` [`crate::supervisor::ChildSpec::versao_requirement`]
1219 /// (2c053c8) / per-`:upgrade-from` [`crate::UpgradeFromEntry::prior_versao`]
1220 /// (75d27a8) per-sub-struct `:versao`-shaped `&str`-return accessors
1221 /// on the sibling per-typed-slot version-carrier axes, extended here
1222 /// to close the second outer top-level [`Caixa`] required-`&str`-
1223 /// carrying axis so the two universal-axis identity-carrying
1224 /// scalars every `defcaixa` form supplies (`:nome` + `:versao`)
1225 /// share the same "one typed dispatch per axis" discipline. Named
1226 /// `versao()` to match the storage field's name; the accessor's
1227 /// identity maps onto the canonical CAIXA-SDLC §I vocabulary the
1228 /// slot's docstring already carries.
1229 #[must_use]
1230 pub fn versao(&self) -> &str {
1231 &self.versao
1232 }
1233
1234 /// Substrate-canonical per-`Caixa` `:kind` universal-axis
1235 /// closed-set-enum discriminant accessor every consumer of the top-
1236 /// level manifest's kind axis keys off — returns the author-declared
1237 /// `:kind` variant verbatim as a [`CaixaKind`], `Copy`-projected
1238 /// from the typed slot's own [`CaixaKind`] storage. Non-optional
1239 /// (`:kind` is a required-axis discriminant every `defcaixa` form
1240 /// must supply alongside `:nome` / `:versao`; the [`Self::from_lisp`]
1241 /// derive rejects an omitted / non-symbol `:kind` at parse time, so
1242 /// a `Caixa` past parse definitionally carries a valid [`CaixaKind`]
1243 /// variant).
1244 ///
1245 /// The `:kind` slot carries the universal-axis closed-set typed-
1246 /// discriminant every substrate-side dispatch keys off (CAIXA-SDLC
1247 /// §I — the primary shape gate every renderer / verifier /
1248 /// operator branches on; the five variants `Biblioteca` /
1249 /// `Binario` / `Servico` / `Supervisor` / `Aplicacao` partition
1250 /// the caixa surface into disjoint runtime contracts) — the typed
1251 /// slot's [`CaixaKind`] accept-set (parse-time-rejected non-symbol
1252 /// values through the derive-macro's symbol-arm gate, exhaustively
1253 /// matched at every downstream dispatch site) maps onto every
1254 /// load-bearing downstream consumer the substrate carries:
1255 ///
1256 /// - [`crate::render::require_kind`]'s per-renderer entry-gate
1257 /// predicate — the canonical two-line
1258 /// `require_kind(caixa, Servico)?` prelude every per-Servico
1259 /// renderer (`caixa-helm`, `caixa-flux`, the future `caixa-otel`
1260 /// / per-Servico OCI packager / M4 `wasm.pleme.io/v1alpha1/
1261 /// ComputeUnit` CR materializer) runs at its entry-point,
1262 /// alongside the [`crate::render::KindMismatch`] error carrier's
1263 /// `actual:` field the diagnostic surfaces to name the offending
1264 /// caixa's variant.
1265 /// - [`Self::aplicacao_view`]'s + [`Self::supervisor_view`]'s
1266 /// per-view kind-gate binding — the two `Option<TypedSpec>`
1267 /// `_view` composers that fold the flat mesh-slot / supervisor-
1268 /// slot columns into their typed sub-spec only when the kind
1269 /// matches (returns `None` otherwise); the future per-Servico
1270 /// M2-view composer (`servico_view`) will follow the same shape.
1271 /// - [`Self::declared_foreign_code_slots`]'s per-slot kind-
1272 /// coherence gate — the `!self.kind.requires_exe()` /
1273 /// `!self.kind.requires_servicos()` predicates that fence
1274 /// each code-surface slot from the wrong owning kind.
1275 /// - [`crate::LayoutInvariants::verify`]'s kind ↔ code-surface
1276 /// coherence gates — the six `caixa.kind == CaixaKind::X` /
1277 /// `caixa.kind != CaixaKind::X` predicates and the four kind-
1278 /// coherence error carriers (`SupervisorOwnsCode` /
1279 /// `AplicacaoOwnsCode` / `MeshSlotsOnNonAplicacao` /
1280 /// `SupervisorSlotsOnNonSupervisor` / `ServicoSlotsOnNonServico`
1281 /// / `ForeignCodeSlot`) which each name the offending caixa's
1282 /// variant in their `kind:` field.
1283 ///
1284 /// Prior to this lift the `.kind` field was accessed inline at
1285 /// twenty-plus production sites across `caixa-core` (the
1286 /// [`crate::render::require_kind`] entry-gate predicate + the
1287 /// [`crate::render::KindMismatch`] `actual:` field, the two `_view`
1288 /// composers, the `declared_foreign_code_slots` per-slot kind-
1289 /// coherence gate, and the six [`crate::LayoutInvariants::verify`]
1290 /// kind ↔ code-surface predicates + four error carriers) — a score
1291 /// of open-coded field-accesses that expressed no compile-time link
1292 /// back to the typed slot. A future extension of the `:kind` axis
1293 /// to a richer author surface — a per-`:kind` sub-variant discriminant
1294 /// (e.g. `Servico(ServicoRuntime)` splitting the current single
1295 /// variant across the wasm-component / legacy-container / native-
1296 /// binary runtime axes the M5 roadmap acknowledges), a per-cluster
1297 /// kind-overlay the M4 CR materializer resolves per-CR (the
1298 /// "cluster policy demotes `Aplicacao` to `Servico` on a single-
1299 /// tenant cluster" arm), a promotion of the plain [`CaixaKind`]
1300 /// enum to a richer `KindWithRuntime` discriminated on the
1301 /// component-model world axis — would have had to be threaded
1302 /// through every open-coded copy in lockstep or the entry gate,
1303 /// the view composers, and the layout invariants would silently
1304 /// disagree on which kind a given [`Caixa`] resolves to. Lifting
1305 /// the resolution to a typed method on the substrate primitive
1306 /// means every downstream consumer of the caixa's per-`Caixa`
1307 /// kind surface reaches for exactly one typed dispatch — the
1308 /// resolver's accept-set migrates as a unit on any future axis
1309 /// addition.
1310 ///
1311 /// First outer top-level [`Caixa`] `Copy`-return required-enum-
1312 /// discriminant accessor — opens the "outer [`Caixa`] `Copy`-return
1313 /// required-discriminant" projection pattern. Sibling in shape to
1314 /// the peer per-`:supervisor` [`crate::supervisor::SupervisorSpec::estrategia`]
1315 /// (eafb619), per-`:placement` [`crate::aplicacao::Placement::estrategia`]
1316 /// (921fe1b), and per-`:children` [`crate::supervisor::ChildSpec::restart`]
1317 /// (dfb4a81) `Copy`-return closed-set-enum discriminant accessors
1318 /// on the sibling nested-spec typed-slot discriminator axes,
1319 /// extended here to the outer top-level [`Caixa`] universal-axis
1320 /// surface. Named `kind()` to match the storage field's name;
1321 /// the accessor's identity maps onto the canonical CAIXA-SDLC §I
1322 /// vocabulary the slot's docstring already carries.
1323 #[must_use]
1324 pub fn kind(&self) -> CaixaKind {
1325 self.kind
1326 }
1327
1328 /// Substrate-canonical per-`Caixa` `:autores` universal-axis
1329 /// maintainer-name-list slice-accessor every consumer of the top-
1330 /// level manifest's maintainer axis keys off — returns the author-
1331 /// declared `:autores` list verbatim as a `&[String]` slice-view over
1332 /// the same backing buffer the raw `self.autores.as_slice()` field
1333 /// access borrows from. Empty-list-carrying (`:autores` is a default-
1334 /// empty axis every `defcaixa` form supplies with an empty `()` when
1335 /// unset; the [`Self::from_lisp`] derive folds an omitted `:autores`
1336 /// through `#[serde(default)]` to `Vec::new()`, so a `Caixa` past
1337 /// parse definitionally carries a `Vec<String>` slot — possibly
1338 /// empty — and the returned `&[String]` degenerates to an empty
1339 /// slice on that arm without any silent `None` collapse).
1340 ///
1341 /// The `:autores` slot carries the universal-axis maintainer-name
1342 /// list every kind of caixa emits under (CAIXA-SDLC §I — the author-
1343 /// facing surface every `defcaixa` form supplies alongside `:nome` /
1344 /// `:versao` / `:kind`; the substrate-wide contact-carrying axis
1345 /// every downstream registry-facing artifact emits under) — the
1346 /// typed slot's `Vec<String>` accept-set (empty-per-entry rejected
1347 /// through [`ManifestError::AutorEmpty`], non-chart-maintainer-shape
1348 /// rejected through [`ManifestError::AutorInvalid`], cross-entry
1349 /// duplicate rejected through [`ManifestError::AutorDuplicate`]) maps
1350 /// onto every load-bearing downstream consumer the substrate carries
1351 /// — the [`Self::validate_autores`] universal-axis empty-per-entry +
1352 /// shape + duplicate gate at caixa-core/src/manifest.rs, the
1353 /// caixa-helm `build_chart_yaml` `maintainers:` fold at
1354 /// caixa-helm/src/lib.rs that walks each entry into a `Maintainer {
1355 /// name, email: None }` record, every future per-`Caixa` registry-
1356 /// facing renderer the CAIXA-SDLC §I roadmap acknowledges (the
1357 /// future `artifacthub.io/maintainers` `Chart.yaml` annotation the
1358 /// caixa-helm docstring alludes to at [`Self::validate_licenca`],
1359 /// the future per-cluster author-notification overlay the M4 CR
1360 /// materializer resolves per-CR).
1361 ///
1362 /// Prior to this lift the `.autores` field was accessed inline at
1363 /// two production sites — [`Self::validate_autores`]'s `for autor
1364 /// in &self.autores` walk that gates every entry through
1365 /// [`ManifestError::AutorEmpty`] / `AutorInvalid` / `AutorDuplicate`,
1366 /// and the caixa-helm `build_chart_yaml` `caixa.autores.iter().map(|a|
1367 /// Maintainer { name: a.clone(), email: None }).collect()` fold that
1368 /// materializes every entry into a `Chart.yaml` `maintainers:` row —
1369 /// two open-coded field-accesses that expressed no compile-time link
1370 /// back to the typed slot. A future extension of the `:autores` axis
1371 /// to a richer author surface — a per-`:autores` structured
1372 /// `Maintainer { name, email, url }` at the storage layer once the
1373 /// substrate absorbs `artifacthub.io/maintainers`' name+email+url
1374 /// tuple, a per-registry `:autores` allowlist the M4 CR materializer
1375 /// enforces per-CR (the "cluster policy demands every author declare
1376 /// an on-file `mailto:` contact" arm), a promotion of the plain
1377 /// `Vec<String>` byte-string list to a richer
1378 /// `Vec<ChartMaintainer>` newtype discriminated on the RFC-5322
1379 /// `<name> [<email>]` grammar the `is_chart_maintainer_name_shape`
1380 /// predicate already resolves through — would have had to be
1381 /// threaded through both open-coded copies in lockstep or the
1382 /// validate gate and the caixa-helm emit path would silently
1383 /// disagree on which authors a given [`Caixa`] resolves to (an
1384 /// author's `:autores ("alice" "bob")` would satisfy validate while
1385 /// the caixa-helm emit path silently rendered a drifted other
1386 /// maintainer list, or vice versa). Lifting the resolution to a
1387 /// typed method on the substrate primitive means every downstream
1388 /// consumer of the caixa's per-`Caixa` maintainer surface reaches
1389 /// for exactly one typed dispatch — the resolver's accept-set
1390 /// migrates as a unit on any future axis addition.
1391 ///
1392 /// First outer top-level [`Caixa`] `&[T]`-return slice-accessor —
1393 /// opens the "outer [`Caixa`] `&[T]` slice" projection pattern the
1394 /// sibling per-`Caixa` `:etiquetas` / `:deps` / `:deps-dev` / `:exe`
1395 /// / `:bibliotecas` / `:servicos` / `:upgrade-from` / `:children`
1396 /// future lifts fold on. Sibling in shape to the peer per-`:supervisor`
1397 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce), per-`:placement`
1398 /// [`crate::aplicacao::Placement::clusters`] (a6e18d7), per-`:membros`
1399 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36), per-`:contratos`
1400 /// [`crate::aplicacao::AplicacaoSpec::contratos`] (0dcc926), and
1401 /// per-`:upgrade-from :instructions` [`crate::upgrade::UpgradeFromEntry::instructions`]
1402 /// (0137e5a) `&[T]`-return slice accessors on the sibling per-M2 /
1403 /// per-M3 typed-slot list axes, extended here to the outer top-level
1404 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1405 /// `&Vec<String>`) because every downstream consumer of the author
1406 /// list treats it as a read-only sequence — the slice-view is the
1407 /// narrowest borrow that supports every present + roadmapped consumer
1408 /// (`.iter()`, `.len()`, `.is_empty()`) without leaking the backing
1409 /// `Vec`'s grow/push/reserve surface no consumer of the typed view
1410 /// reaches for (the storage-side `Vec` remains reachable through the
1411 /// `pub autores` field for the mutation-carrying serde round-trip and
1412 /// per-test fixture-mutation paths). Named `autores()` to match the
1413 /// storage field's name; the accessor's identity maps onto the
1414 /// canonical CAIXA-SDLC §I vocabulary the slot's docstring already
1415 /// carries.
1416 #[must_use]
1417 pub fn autores(&self) -> &[String] {
1418 self.autores.as_slice()
1419 }
1420
1421 /// Substrate-canonical per-`Caixa` `:etiquetas` universal-axis
1422 /// registry-search-tag-list slice-accessor every consumer of the
1423 /// top-level manifest's topical-tag axis keys off — returns the
1424 /// author-declared `:etiquetas` list verbatim as a `&[String]`
1425 /// slice-view over the same backing buffer the raw
1426 /// `self.etiquetas.as_slice()` field access borrows from. Empty-
1427 /// list-carrying (`:etiquetas` is a default-empty axis every
1428 /// `defcaixa` form supplies with an empty `()` when unset; the
1429 /// [`Self::from_lisp`] derive folds an omitted `:etiquetas` through
1430 /// `#[serde(default)]` to `Vec::new()`, so a `Caixa` past parse
1431 /// definitionally carries a `Vec<String>` slot — possibly empty —
1432 /// and the returned `&[String]` degenerates to an empty slice on
1433 /// that arm without any silent `None` collapse).
1434 ///
1435 /// The `:etiquetas` slot carries the universal-axis topical-tag
1436 /// list every kind of caixa emits under (CAIXA-SDLC §I — the
1437 /// author-facing surface every `defcaixa` form supplies alongside
1438 /// `:nome` / `:versao` / `:kind`; the substrate-wide registry-
1439 /// search-facing axis every downstream registry-facing artifact
1440 /// emits under) — the typed slot's `Vec<String>` accept-set
1441 /// (empty-per-entry rejected through [`ManifestError::EtiquetaEmpty`],
1442 /// non-chart-keyword-shape rejected through
1443 /// [`ManifestError::EtiquetaInvalid`], cross-entry duplicate
1444 /// rejected through [`ManifestError::EtiquetaDuplicate`]) maps onto
1445 /// every load-bearing downstream consumer the substrate carries —
1446 /// the [`Self::validate_etiquetas`] universal-axis empty-per-entry
1447 /// + shape + duplicate gate at caixa-core/src/manifest.rs, the
1448 /// caixa-helm `build_chart_yaml` `keywords:` fold at
1449 /// caixa-helm/src/lib.rs that walks each entry into the rendered
1450 /// `Chart.yaml` `keywords:` array (chained with the
1451 /// [`crate::LAREIRA_CHART_KEYWORDS`] substrate-wide floor set and
1452 /// dedup'd through a `BTreeSet` at emit time), every future per-
1453 /// `Caixa` registry-facing renderer the CAIXA-SDLC §I roadmap
1454 /// acknowledges (the future `artifacthub.io/keywords` `Chart.yaml`
1455 /// annotation, the future per-cluster tag-notification overlay the
1456 /// M4 CR materializer resolves per-CR).
1457 ///
1458 /// Prior to this lift the `.etiquetas` field was accessed inline at
1459 /// two production sites — [`Self::validate_etiquetas`]'s `for
1460 /// etiqueta in &self.etiquetas` walk that gates every entry through
1461 /// [`ManifestError::EtiquetaEmpty`] / `EtiquetaInvalid` /
1462 /// `EtiquetaDuplicate`, and the caixa-helm `build_chart_yaml`
1463 /// `caixa.etiquetas.iter().cloned().chain(...)` fold that
1464 /// materializes every entry into a `Chart.yaml` `keywords:` row —
1465 /// two open-coded field-accesses that expressed no compile-time
1466 /// link back to the typed slot. A future extension of the
1467 /// `:etiquetas` axis to a richer tag surface — a per-`:etiquetas`
1468 /// structured `ChartKeyword { name, uri, category }` at the storage
1469 /// layer once the substrate absorbs `artifacthub.io/keywords`
1470 /// richer tag tuple, a per-registry `:etiquetas` allowlist the M4
1471 /// CR materializer enforces per-CR (the "cluster policy demands
1472 /// every tag come from a substrate-approved taxonomy" arm), a
1473 /// promotion of the plain `Vec<String>` byte-string list to a
1474 /// richer `Vec<ChartKeyword>` newtype discriminated on the DNS-
1475 /// 1123-label-shaped grammar the `is_chart_keyword_shape` predicate
1476 /// already resolves through — would have had to be threaded through
1477 /// both open-coded copies in lockstep or the validate gate and the
1478 /// caixa-helm emit path would silently disagree on which tags a
1479 /// given [`Caixa`] resolves to (an author's `:etiquetas ("demo"
1480 /// "aplicacao")` would satisfy validate while the caixa-helm emit
1481 /// path silently rendered a drifted other keyword list, or vice
1482 /// versa). Lifting the resolution to a typed method on the
1483 /// substrate primitive means every downstream consumer of the
1484 /// caixa's per-`Caixa` topical-tag surface reaches for exactly one
1485 /// typed dispatch — the resolver's accept-set migrates as a unit
1486 /// on any future axis addition.
1487 ///
1488 /// Second outer top-level [`Caixa`] `&[T]`-return slice-accessor —
1489 /// folds on the "outer [`Caixa`] `&[T]` slice" projection pattern
1490 /// [`Self::autores`] (b5d813f) opened, sibling in shape and
1491 /// idiom. The remaining unlifted outer-`Caixa` slice-carrying axes
1492 /// (`:deps` / `:deps-dev` / `:exe` / `:bibliotecas` / `:servicos`
1493 /// / `:upgrade-from` / `:children` / `:membros` / `:contratos`)
1494 /// fold onto the same pattern in future lifts. Sibling in shape to
1495 /// the peer per-`:supervisor`
1496 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
1497 /// per-`:placement` [`crate::aplicacao::Placement::clusters`]
1498 /// (a6e18d7), per-`:membros`
1499 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
1500 /// per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
1501 /// (0dcc926), and per-`:upgrade-from :instructions`
1502 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1503 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1504 /// typed-slot list axes, extended here to the outer top-level
1505 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1506 /// `&Vec<String>`) because every downstream consumer of the tag
1507 /// list treats it as a read-only sequence — the slice-view is the
1508 /// narrowest borrow that supports every present + roadmapped
1509 /// consumer (`.iter()`, `.len()`, `.is_empty()`) without leaking
1510 /// the backing `Vec`'s grow/push/reserve surface no consumer of
1511 /// the typed view reaches for (the storage-side `Vec` remains
1512 /// reachable through the `pub etiquetas` field for the mutation-
1513 /// carrying serde round-trip and per-test fixture-mutation paths).
1514 /// Named `etiquetas()` to match the storage field's name; the
1515 /// accessor's identity maps onto the canonical CAIXA-SDLC §I
1516 /// vocabulary the slot's docstring already carries.
1517 #[must_use]
1518 pub fn etiquetas(&self) -> &[String] {
1519 self.etiquetas.as_slice()
1520 }
1521
1522 /// Substrate-canonical per-`Caixa` `:bibliotecas` universal-axis
1523 /// library-source-path-list slice-accessor every consumer of the
1524 /// top-level manifest's Biblioteca-source axis keys off — returns
1525 /// the author-declared `:bibliotecas` list verbatim as a
1526 /// `&[String]` slice-view over the same backing buffer the raw
1527 /// `self.bibliotecas.as_slice()` field access borrows from. Empty-
1528 /// list-carrying (`:bibliotecas` is a default-empty axis every
1529 /// `defcaixa` form supplies with an empty `()` when unset; the
1530 /// [`Self::from_lisp`] derive folds an omitted `:bibliotecas`
1531 /// through `#[serde(default)]` to `Vec::new()`, so a `Caixa` past
1532 /// parse definitionally carries a `Vec<String>` slot — possibly
1533 /// empty — and the returned `&[String]` degenerates to an empty
1534 /// slice on that arm without any silent `None` collapse).
1535 ///
1536 /// The `:bibliotecas` slot carries the universal-axis lisp-library
1537 /// entry-path list every `:kind Biblioteca` caixa emits under
1538 /// (CAIXA-SDLC §I — the author-facing surface every `defcaixa`
1539 /// form supplies alongside `:nome` / `:versao` / `:kind`; the
1540 /// substrate-wide library-carrier axis every downstream
1541 /// authoring-facing consumer keys off) — the typed slot's
1542 /// `Vec<String>` accept-set (empty-per-entry rejected through
1543 /// [`ManifestError::CodePathEmpty { slot: ":bibliotecas" }`],
1544 /// non-sandboxed-relative-shape rejected through
1545 /// [`ManifestError::CodePathShape`], non-`.lisp`-extension rejected
1546 /// through [`ManifestError::CodePathNonLispExtension`], cross-entry
1547 /// duplicate rejected through [`ManifestError::CodePathDuplicate`])
1548 /// maps onto every load-bearing downstream consumer the substrate
1549 /// carries — the [`crate::LayoutInvariants`] Biblioteca-arm
1550 /// empty-check + per-entry file-exists loop at
1551 /// caixa-core/src/layout.rs that gates each entry through
1552 /// [`crate::LayoutError::MissingLib`] / `MissingEntry`, the
1553 /// [`Self::validate_code_paths`] per-slot shape gate at
1554 /// caixa-core/src/manifest.rs that walks each entry through the
1555 /// sandbox-relative / `.lisp`-extension / cross-entry duplicate
1556 /// gates, the `feira build` per-entry `tatara_lisp::read` parse
1557 /// walk at caixa-feira/src/cmd/build.rs that phase-1-checks each
1558 /// declared library file for lexical / structural errors before
1559 /// downstream `importar` resolution, every future per-`Caixa`
1560 /// library-facing renderer the CAIXA-SDLC §I roadmap acknowledges
1561 /// (the future `tatara-lispc` compilation entry the docstring at
1562 /// caixa-feira/src/cmd/build.rs alludes to, the future per-cluster
1563 /// bytecode-caching overlay the M4 CR materializer resolves per-CR,
1564 /// the future `caixa-lsp` per-library semantic-token stream the
1565 /// caixa-lsp docstring roadmaps).
1566 ///
1567 /// Prior to this lift the `.bibliotecas` field was accessed inline
1568 /// at three production sites — [`crate::LayoutInvariants`]'s
1569 /// `caixa.bibliotecas.is_empty()` `MissingLib`-arm gate + `for p
1570 /// in &caixa.bibliotecas` `MissingEntry` walk that gates each
1571 /// declared library path through the on-disk-existence check,
1572 /// the compound-code-path `has_code = !caixa.bibliotecas.is_empty()
1573 /// || !caixa.exe.is_empty() || !caixa.servicos.is_empty()` OR-fold
1574 /// on the [`crate::LayoutError::SupervisorOwnsCode`] /
1575 /// `AplicacaoOwnsCode` kind-coherence gate, and the `feira build`
1576 /// per-entry `for entry in &caixa.bibliotecas` + `caixa.bibliotecas.
1577 /// len()` phase-1 tatara-lispc-precursor parse walk — three open-
1578 /// coded field-accesses that expressed no compile-time link back
1579 /// to the typed slot. A future extension of the `:bibliotecas`
1580 /// axis to a richer library surface — a per-`:bibliotecas`
1581 /// structured `BibliotecaEntry { path, edition, exports }` at the
1582 /// storage layer once the substrate absorbs the per-library
1583 /// language-edition + explicit-exports tuple the tatara-lisp
1584 /// module-system roadmap acknowledges, a per-registry
1585 /// `:bibliotecas` allowlist the M4 CR materializer enforces
1586 /// per-CR (the "cluster policy demands every biblioteca declare
1587 /// its own :edicao" arm), a promotion of the plain `Vec<String>`
1588 /// byte-string list to a richer `Vec<LibraryPath>` newtype
1589 /// discriminated on the `lib/<nome>.lisp`-shape grammar the
1590 /// [`crate::render::is_sandboxed_relative_path`] +
1591 /// [`crate::render::is_lisp_extension`] predicates already resolve
1592 /// through — would have had to be threaded through all three
1593 /// open-coded copies in lockstep or the layout gate, the shape
1594 /// validator, and the `feira build` phase-1 parse walk would
1595 /// silently disagree on which library paths a given [`Caixa`]
1596 /// resolves to (an author's `:bibliotecas ("lib/foo.lisp"
1597 /// "lib/bar.lisp")` would satisfy layout while `feira build`
1598 /// silently parsed a drifted other list, or vice versa). Lifting
1599 /// the resolution to a typed method on the substrate primitive
1600 /// means every downstream consumer of the caixa's per-`Caixa`
1601 /// library-source surface reaches for exactly one typed dispatch
1602 /// — the resolver's accept-set migrates as a unit on any future
1603 /// axis addition.
1604 ///
1605 /// Third outer top-level [`Caixa`] `&[T]`-return slice-accessor —
1606 /// folds on the "outer [`Caixa`] `&[T]` slice" projection pattern
1607 /// [`Self::autores`] (b5d813f) opened and [`Self::etiquetas`]
1608 /// (78c7d3c) folded on, sibling in shape and idiom. The remaining
1609 /// unlifted outer-`Caixa` slice-carrying axes (`:deps` /
1610 /// `:deps-dev` / `:exe` / `:servicos` / `:upgrade-from` /
1611 /// `:children` / `:membros` / `:contratos`) fold onto the same
1612 /// pattern in future lifts. Sibling in shape to the peer
1613 /// per-`:supervisor`
1614 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
1615 /// per-`:placement` [`crate::aplicacao::Placement::clusters`]
1616 /// (a6e18d7), per-`:membros`
1617 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
1618 /// per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
1619 /// (0dcc926), and per-`:upgrade-from :instructions`
1620 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1621 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1622 /// typed-slot list axes, extended here to the outer top-level
1623 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1624 /// `&Vec<String>`) because every downstream consumer of the
1625 /// library-source list treats it as a read-only sequence — the
1626 /// slice-view is the narrowest borrow that supports every
1627 /// present + roadmapped consumer (`.iter()`, `.len()`,
1628 /// `.is_empty()`) without leaking the backing `Vec`'s
1629 /// grow/push/reserve surface no consumer of the typed view
1630 /// reaches for (the storage-side `Vec` remains reachable through
1631 /// the `pub bibliotecas` field for the mutation-carrying serde
1632 /// round-trip and per-test fixture-mutation paths). Named
1633 /// `bibliotecas()` to match the storage field's name; the
1634 /// accessor's identity maps onto the canonical CAIXA-SDLC §I
1635 /// vocabulary the slot's docstring already carries.
1636 #[must_use]
1637 pub fn bibliotecas(&self) -> &[String] {
1638 self.bibliotecas.as_slice()
1639 }
1640
1641 /// Substrate-canonical per-`Caixa` `:exe` universal-axis
1642 /// nix-built-executable-entry-path-list slice-accessor every consumer
1643 /// of the top-level manifest's Binario-executable axis keys off —
1644 /// returns the author-declared `:exe` list verbatim as a `&[String]`
1645 /// slice-view over the same backing buffer the raw
1646 /// `self.exe.as_slice()` field access borrows from. Empty-list-
1647 /// carrying (`:exe` is a default-empty axis every `defcaixa` form
1648 /// supplies with an empty `()` when unset; the [`Self::from_lisp`]
1649 /// derive folds an omitted `:exe` through `#[serde(default)]` to
1650 /// `Vec::new()`, so a `Caixa` past parse definitionally carries a
1651 /// `Vec<String>` slot — possibly empty — and the returned `&[String]`
1652 /// degenerates to an empty slice on that arm without any silent
1653 /// `None` collapse).
1654 ///
1655 /// The `:exe` slot carries the universal-axis nix-built executable
1656 /// entry-path list every `:kind Binario` caixa emits under
1657 /// (CAIXA-SDLC §I — the author-facing surface every `defcaixa`
1658 /// form supplies alongside `:nome` / `:versao` / `:kind`; the
1659 /// substrate-wide `exe/`-directory-fenced entry-carrier axis every
1660 /// downstream flake-build-facing consumer keys off) — the typed
1661 /// slot's `Vec<String>` accept-set (empty-per-entry rejected
1662 /// through [`ManifestError::CodePathEmpty { slot: ":exe" }`],
1663 /// non-sandboxed-relative-shape rejected through
1664 /// [`ManifestError::CodePathShape`], cross-entry duplicate rejected
1665 /// through [`ManifestError::CodePathDuplicate`], out-of-`exe/`-
1666 /// directory paths rejected past the layout's
1667 /// [`crate::LayoutError::ExeOutsideDir`] `starts_with` fence) maps
1668 /// onto every load-bearing downstream consumer the substrate carries
1669 /// — the [`crate::LayoutInvariants`] Binario-arm empty-check +
1670 /// per-entry file-exists + `exe/`-directory-fence loop at
1671 /// caixa-core/src/layout.rs that gates each entry through
1672 /// [`crate::LayoutError::BinarioWithoutExe`] / `MissingEntry` /
1673 /// `ExeOutsideDir`, the compound `has_code` OR-fold on the
1674 /// [`crate::LayoutError::SupervisorOwnsCode`] /
1675 /// [`crate::LayoutError::AplicacaoOwnsCode`] kind-coherence gate
1676 /// that fences code-surface slots off from the two no-code kinds,
1677 /// [`Self::declared_foreign_code_slots`]'s `!self.exe.is_empty()`
1678 /// arm on the [`crate::LayoutError::ForeignCodeSlot`] gate that
1679 /// fences the `:exe` code surface off from every non-Binario code-
1680 /// running kind, [`Self::validate_code_paths`]'s per-slot shape gate
1681 /// that walks each entry through the sandbox-relative / cross-entry
1682 /// duplicate gates, every future per-`Caixa` executable-facing
1683 /// renderer the CAIXA-SDLC §I roadmap acknowledges (the future
1684 /// `caixa-flake` per-Binario `packages.<system>.<nome>` derivation
1685 /// entry the caixa-flake docstring roadmaps, the future per-cluster
1686 /// `nix-store` overlay the M4 CR materializer resolves per-CR, the
1687 /// future `feira nix` per-executable Binario-target emit path).
1688 ///
1689 /// Prior to this lift the `.exe` field was accessed inline at three
1690 /// production sites — the compound-code-path `has_code =
1691 /// !caixa.bibliotecas().is_empty() || !caixa.exe.is_empty() ||
1692 /// !caixa.servicos.is_empty()` OR-fold on the
1693 /// [`crate::LayoutError::SupervisorOwnsCode`] /
1694 /// `AplicacaoOwnsCode` kind-coherence gate, the Binario-arm
1695 /// `caixa.exe.is_empty()` [`crate::LayoutError::BinarioWithoutExe`]
1696 /// gate, the per-entry `for p in &caixa.exe`
1697 /// `MissingEntry`/`ExeOutsideDir` walk, and the
1698 /// [`Self::declared_foreign_code_slots`]'s
1699 /// `!self.exe.is_empty()` arm on the `ForeignCodeSlot` gate — four
1700 /// open-coded field-accesses that expressed no compile-time link
1701 /// back to the typed slot. A future extension of the `:exe` axis
1702 /// to a richer executable surface — a per-`:exe` structured
1703 /// `BinarioEntry { path, wrapper, capabilities }` at the storage
1704 /// layer once the substrate absorbs the per-executable
1705 /// nix-wrapper + linux-capabilities tuple the CAIXA-SDLC §I
1706 /// executable roadmap acknowledges, a per-registry `:exe` allowlist
1707 /// the M4 CR materializer enforces per-CR (the "cluster policy
1708 /// demands every Binario declare an explicit `:wrapper`" arm), a
1709 /// promotion of the plain `Vec<String>` byte-string list to a
1710 /// richer `Vec<ExecutablePath>` newtype discriminated on the
1711 /// `exe/<nome>`-shape grammar the layout's `starts_with(exe_dir)`
1712 /// fence already resolves through — would have had to be threaded
1713 /// through all four open-coded copies in lockstep or the layout
1714 /// gate, the shape validator, and the `feira nix` emit path would
1715 /// silently disagree on which executable paths a given [`Caixa`]
1716 /// resolves to (an author's `:exe ("exe/cli" "exe/serve")` would
1717 /// satisfy layout while `feira nix` silently packaged a drifted
1718 /// other list, or vice versa). Lifting the resolution to a typed
1719 /// method on the substrate primitive means every downstream
1720 /// consumer of the caixa's per-`Caixa` executable-source surface
1721 /// reaches for exactly one typed dispatch — the resolver's accept-
1722 /// set migrates as a unit on any future axis addition.
1723 ///
1724 /// Fourth outer top-level [`Caixa`] `&[T]`-return slice-accessor —
1725 /// folds on the "outer [`Caixa`] `&[T]` slice" projection pattern
1726 /// [`Self::autores`] (b5d813f) opened, [`Self::etiquetas`]
1727 /// (78c7d3c) folded on, and [`Self::bibliotecas`] (8a36c23) closed
1728 /// the universal-axis text-tag family of. Opens the outer-`Caixa`
1729 /// foreign-code-slot `&[T]` sub-family the sibling `:servicos`
1730 /// future lift closes onto (per the trio of code-surface list slots
1731 /// the [`Self::validate_code_paths`] per-slot dispatch tuple
1732 /// already carries — `:bibliotecas` + `:exe` + `:servicos`, of which
1733 /// `:bibliotecas` landed at 8a36c23 and `:servicos` remains as the
1734 /// last unlifted code-surface slot). Sibling in shape to the peer
1735 /// per-`:supervisor` [`crate::supervisor::SupervisorSpec::children`]
1736 /// (bc92bce), per-`:placement`
1737 /// [`crate::aplicacao::Placement::clusters`] (a6e18d7),
1738 /// per-`:membros` [`crate::aplicacao::AplicacaoSpec::membros`]
1739 /// (6c77e36), per-`:contratos`
1740 /// [`crate::aplicacao::AplicacaoSpec::contratos`] (0dcc926), and
1741 /// per-`:upgrade-from :instructions`
1742 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1743 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1744 /// typed-slot list axes, extended here to the outer top-level
1745 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1746 /// `&Vec<String>`) because every downstream consumer of the
1747 /// executable-source list treats it as a read-only sequence — the
1748 /// slice-view is the narrowest borrow that supports every
1749 /// present + roadmapped consumer (`.iter()`, `.len()`,
1750 /// `.is_empty()`) without leaking the backing `Vec`'s
1751 /// grow/push/reserve surface no consumer of the typed view
1752 /// reaches for (the storage-side `Vec` remains reachable through
1753 /// the `pub exe` field for the mutation-carrying serde
1754 /// round-trip and per-test fixture-mutation paths). Named `exe()`
1755 /// to match the storage field's name; the accessor's identity
1756 /// maps onto the canonical CAIXA-SDLC §I vocabulary the slot's
1757 /// docstring already carries.
1758 #[must_use]
1759 pub fn exe(&self) -> &[String] {
1760 self.exe.as_slice()
1761 }
1762
1763 /// Substrate-canonical per-`Caixa` `:servicos` universal-axis
1764 /// ComputeUnit-CR-YAML-entry-path-list slice-accessor every consumer
1765 /// of the top-level manifest's Servico-component axis keys off —
1766 /// returns the author-declared `:servicos` list verbatim as a
1767 /// `&[String]` slice-view over the same backing buffer the raw
1768 /// `self.servicos.as_slice()` field access borrows from. Empty-list-
1769 /// carrying (`:servicos` is a default-empty axis every `defcaixa`
1770 /// form supplies with an empty `()` when unset; the
1771 /// [`Self::from_lisp`] derive folds an omitted `:servicos` through
1772 /// `#[serde(default)]` to `Vec::new()`, so a `Caixa` past parse
1773 /// definitionally carries a `Vec<String>` slot — possibly empty —
1774 /// and the returned `&[String]` degenerates to an empty slice on
1775 /// that arm without any silent `None` collapse).
1776 ///
1777 /// The `:servicos` slot carries the universal-axis
1778 /// `.computeunit.yaml` ComputeUnit-CR entry-path list every
1779 /// `:kind Servico` caixa emits under (CAIXA-SDLC §I — the
1780 /// author-facing surface every `defcaixa` form supplies alongside
1781 /// `:nome` / `:versao` / `:kind`; the substrate-wide
1782 /// `servicos/`-directory-fenced entry-carrier axis every downstream
1783 /// Servico-facing renderer keys off) — the typed slot's
1784 /// `Vec<String>` accept-set (empty-per-entry rejected through
1785 /// [`ManifestError::CodePathEmpty { slot: ":servicos" }`],
1786 /// non-sandboxed-relative-shape rejected through
1787 /// [`ManifestError::CodePathShape`], non-`.computeunit.yaml`
1788 /// extension rejected through
1789 /// [`ManifestError::CodePathNonComputeUnitYamlExtension`], cross-
1790 /// entry duplicate rejected through
1791 /// [`ManifestError::CodePathDuplicate`], `len != 1` rejected by the
1792 /// V0 [`crate::ServicoCountMismatch`] gate on the per-Servico
1793 /// renderer entry-points, out-of-`servicos/`-directory paths
1794 /// rejected past the layout's [`crate::LayoutError::ServicoOutsideDir`]
1795 /// `starts_with` fence) maps onto every load-bearing downstream
1796 /// consumer the substrate carries — the [`crate::LayoutInvariants`]
1797 /// Servico-arm empty-check + per-entry file-exists + `servicos/`-
1798 /// directory-fence loop at caixa-core/src/layout.rs that gates each
1799 /// entry through [`crate::LayoutError::ServicoWithoutServicos`] /
1800 /// `MissingEntry` / `ServicoOutsideDir`, the compound `has_code`
1801 /// OR-fold on the [`crate::LayoutError::SupervisorOwnsCode`] /
1802 /// [`crate::LayoutError::AplicacaoOwnsCode`] kind-coherence gate
1803 /// that fences code-surface slots off from the two no-code kinds,
1804 /// [`Self::declared_foreign_code_slots`]'s
1805 /// `!self.servicos.is_empty()` arm on the
1806 /// [`crate::LayoutError::ForeignCodeSlot`] gate that fences the
1807 /// `:servicos` code surface off from every non-Servico code-running
1808 /// kind, [`Self::validate_code_paths`]'s per-slot shape gate that
1809 /// walks each entry through the sandbox-relative / `.computeunit.
1810 /// yaml`-extension / cross-entry duplicate gates, the
1811 /// [`crate::require_single_servico`] V0 singularity gate every
1812 /// per-Servico renderer entry-point runs through
1813 /// [`crate::require_v0_servico_shape`], the `feira chart` /
1814 /// `feira deploy` per-verb `first_servico_path` walk at
1815 /// caixa-feira/src/cmd/chart.rs that resolves the singleton
1816 /// ComputeUnit-CR file, every future per-`Caixa` Servico-facing
1817 /// renderer the CAIXA-SDLC §I roadmap acknowledges (the future
1818 /// per-Servico OCI packager, the future M4
1819 /// `wasm.pleme.io/v1alpha1/ComputeUnit` CR materializer, the future
1820 /// per-Servico OTel collector-config emit).
1821 ///
1822 /// Prior to this lift the `.servicos` field was accessed inline at
1823 /// five production sites — the compound-code-path `has_code =
1824 /// !caixa.bibliotecas().is_empty() || !caixa.exe().is_empty() ||
1825 /// !caixa.servicos.is_empty()` OR-fold on the
1826 /// [`crate::LayoutError::SupervisorOwnsCode`] /
1827 /// `AplicacaoOwnsCode` kind-coherence gate, the Servico-arm
1828 /// `caixa.servicos.is_empty()`
1829 /// [`crate::LayoutError::ServicoWithoutServicos`] gate, the
1830 /// per-entry `for p in &caixa.servicos`
1831 /// `MissingEntry`/`ServicoOutsideDir` walk, the
1832 /// [`Self::declared_foreign_code_slots`]'s
1833 /// `!self.servicos.is_empty()` arm on the `ForeignCodeSlot` gate,
1834 /// and the [`crate::require_single_servico`] V0 count gate's
1835 /// `caixa.servicos.len() == 1` / `caixa.servicos.len()` count
1836 /// projection (both the accept-arm predicate and the
1837 /// diagnostic-carrying `ServicoCountMismatch { count }`
1838 /// projection) — five open-coded field-accesses across three
1839 /// crates that expressed no compile-time link back to the typed
1840 /// slot. A future extension of the `:servicos` axis to a richer
1841 /// component surface — a per-`:servicos` structured
1842 /// `ServicoEntry { path, world, capabilities }` at the storage
1843 /// layer once the substrate absorbs the per-component WIT-world +
1844 /// capability-set tuple the CAIXA-SDLC §I Servico roadmap
1845 /// acknowledges, a per-registry `:servicos` allowlist the M4 CR
1846 /// materializer enforces per-CR (the "cluster policy demands every
1847 /// Servico declare an explicit `:world`" arm), a promotion of the
1848 /// plain `Vec<String>` byte-string list to a richer
1849 /// `Vec<ComputeUnitPath>` newtype discriminated on the
1850 /// `servicos/<nome>.computeunit.yaml`-shape grammar the layout's
1851 /// `starts_with(servicos_dir)` fence and the
1852 /// [`crate::render::is_computeunit_yaml_extension`] predicate
1853 /// already resolve through, a promotion of the V0 singleton
1854 /// contract to a multi-component `Vec<ComputeUnitPath>` past the M5
1855 /// component-model multi-world boundary — would have had to be
1856 /// threaded through all five open-coded copies in lockstep or the
1857 /// layout gate, the shape validator, the V0 count gate, and the
1858 /// `feira chart` / `feira deploy` entry-point walks would silently
1859 /// disagree on which ComputeUnit-CR paths a given [`Caixa`]
1860 /// resolves to (an author's `:servicos ("servicos/foo.computeunit.
1861 /// yaml")` would satisfy layout while `feira chart` silently
1862 /// packaged a drifted other list, or vice versa). Lifting the
1863 /// resolution to a typed method on the substrate primitive means
1864 /// every downstream consumer of the caixa's per-`Caixa`
1865 /// ComputeUnit-CR-source surface reaches for exactly one typed
1866 /// dispatch — the resolver's accept-set migrates as a unit on any
1867 /// future axis addition.
1868 ///
1869 /// Fifth and final outer top-level [`Caixa`] `&[T]`-return slice-
1870 /// accessor — folds on the "outer [`Caixa`] `&[T]` slice"
1871 /// projection pattern [`Self::autores`] (b5d813f) opened,
1872 /// [`Self::etiquetas`] (78c7d3c) folded on, [`Self::bibliotecas`]
1873 /// (8a36c23) closed the universal-axis text-tag family of, and
1874 /// [`Self::exe`] (65d9527) opened the foreign-code-slot sub-family
1875 /// of. Closes the outer-`Caixa` foreign-code-slot `&[T]` sub-family
1876 /// — with `:bibliotecas`, `:exe`, and `:servicos` now each carrying
1877 /// a substrate-canonical slice accessor, the trio of code-surface
1878 /// list slots the [`Self::validate_code_paths`] per-slot dispatch
1879 /// tuple carries is complete on the typed dispatch surface (the
1880 /// internal `[(":bibliotecas", &self.bibliotecas, ..), (":exe",
1881 /// &self.exe, ..), (":servicos", &self.servicos, ..)]` per-slot
1882 /// dispatch tuple's homogeneous `&Vec<String>`-typed shape blocks a
1883 /// per-element accessor swap in isolation — a future companion lift
1884 /// promotes the tuple's element type to `&[String]` and threads the
1885 /// triple of typed dispatches through as a unit). Sibling in shape
1886 /// to the peer per-`:supervisor`
1887 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
1888 /// per-`:placement` [`crate::aplicacao::Placement::clusters`]
1889 /// (a6e18d7), per-`:membros`
1890 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
1891 /// per-`:contratos`
1892 /// [`crate::aplicacao::AplicacaoSpec::contratos`] (0dcc926), and
1893 /// per-`:upgrade-from :instructions`
1894 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1895 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1896 /// typed-slot list axes, extended here to the outer top-level
1897 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1898 /// `&Vec<String>`) because every downstream consumer of the
1899 /// ComputeUnit-CR-source list treats it as a read-only sequence —
1900 /// the slice-view is the narrowest borrow that supports every
1901 /// present + roadmapped consumer (`.iter()`, `.len()`,
1902 /// `.is_empty()`, `.first()`) without leaking the backing `Vec`'s
1903 /// grow/push/reserve surface no consumer of the typed view reaches
1904 /// for (the storage-side `Vec` remains reachable through the
1905 /// `pub servicos` field for the mutation-carrying serde round-trip
1906 /// and per-test fixture-mutation paths, and for the
1907 /// [`Self::validate_code_paths`] per-slot dispatch tuple whose
1908 /// homogeneous-element-type shape carries the raw field access
1909 /// until the trio-closure lift promotes the tuple as a unit).
1910 /// Named `servicos()` to match the storage field's name; the
1911 /// accessor's identity maps onto the canonical CAIXA-SDLC §I
1912 /// vocabulary the slot's docstring already carries.
1913 #[must_use]
1914 pub fn servicos(&self) -> &[String] {
1915 self.servicos.as_slice()
1916 }
1917
1918 /// Substrate-canonical per-`Caixa` `:deps` universal-axis
1919 /// runtime-dependency-declaration-list slice-accessor every consumer
1920 /// of the top-level manifest's runtime-dep-graph axis keys off —
1921 /// returns the author-declared `:deps` list verbatim as a `&[Dep]`
1922 /// slice-view over the same backing buffer the raw
1923 /// `self.deps.as_slice()` field access borrows from. Empty-list-
1924 /// carrying (`:deps` is a default-empty axis every `defcaixa` form
1925 /// supplies with an empty `()` when unset; the [`Self::from_lisp`]
1926 /// derive folds an omitted `:deps` through `#[serde(default)]` to
1927 /// `Vec::new()`, so a `Caixa` past parse definitionally carries a
1928 /// `Vec<Dep>` slot — possibly empty — and the returned `&[Dep]`
1929 /// degenerates to an empty slice on that arm without any silent
1930 /// `None` collapse).
1931 ///
1932 /// The `:deps` slot carries the universal-axis runtime dependency
1933 /// list every kind of caixa emits under (CAIXA-SDLC §I — the author-
1934 /// facing surface every `defcaixa` form supplies alongside `:nome` /
1935 /// `:versao` / `:kind`; the substrate-wide runtime-closure-input axis
1936 /// every downstream resolver-facing artifact emits under) — the
1937 /// typed slot's `Vec<Dep>` accept-set (empty-`:nome` rejected through
1938 /// [`DepError::NomeEmpty`], non-DNS-1123-label `:nome` rejected
1939 /// through [`DepError::NomeInvalid`], malformed `:versao` rejected
1940 /// through [`DepError::VersaoInvalid`], empty `:fonte.repo` rejected
1941 /// through [`DepError::FonteRepoEmpty`], within-list duplicate `:nome`
1942 /// rejected through [`DepError::DuplicateNome { list: ":deps" }`])
1943 /// maps onto every load-bearing downstream consumer the substrate
1944 /// carries — the [`Self::validate_deps`] per-entry
1945 /// [`Dep::validate`] + within-list dedup walk at
1946 /// caixa-core/src/manifest.rs, the [`crate::dep::validate_no_self_dep`]
1947 /// cross-list self-reference gate at caixa-core/src/layout.rs that
1948 /// checks each entry against the caixa's own `:nome`, the
1949 /// caixa-resolver `for dep in &root.deps` closure walk at
1950 /// caixa-resolver/src/resolve.rs that seeds every git-clone target
1951 /// through the resolver's [`crate::Dep`]-keyed pipeline, the
1952 /// caixa-crd `caixa.deps.iter().map(dep_into_ref).collect()` fold at
1953 /// caixa-crd/src/conversion.rs that materializes each entry into the
1954 /// K8s `Caixa` CR's `spec.deps` field, every future per-`Caixa`
1955 /// resolver-facing renderer the CAIXA-SDLC §I roadmap acknowledges
1956 /// (the future per-cluster runtime-closure-audit overlay the M4 CR
1957 /// materializer resolves per-CR, the future `lacre.lisp` BLAKE3-
1958 /// closure emit walk the caixa-resolver docstring roadmaps).
1959 ///
1960 /// First outer top-level [`Caixa`] `&[Dep]`-return slice-accessor —
1961 /// opens the outer-`Caixa` dependency-slot `&[Dep]` sub-family the
1962 /// sibling `:deps-dev` future lift closes on. Peer of the closed
1963 /// outer-`Caixa` foreign-code-slot `&[String]` sub-family
1964 /// ([`Self::bibliotecas`] 8a36c23, [`Self::exe`] 65d9527,
1965 /// [`Self::servicos`] 611f78b) and the outer-`Caixa` universal-axis
1966 /// text-tag family ([`Self::autores`] b5d813f, [`Self::etiquetas`]
1967 /// 78c7d3c) — extends the "outer [`Caixa`] `&[T]` slice" projection
1968 /// pattern onto a novel element-type axis (`Dep` composite vs the
1969 /// prior sibling family's `String` scalar). Sibling in shape to the
1970 /// peer per-`:supervisor`
1971 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
1972 /// per-`:placement` [`crate::aplicacao::Placement::clusters`]
1973 /// (a6e18d7), per-`:membros`
1974 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
1975 /// per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
1976 /// (0dcc926), and per-`:upgrade-from :instructions`
1977 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1978 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1979 /// typed-slot list axes, extended here to the outer top-level
1980 /// [`Caixa`] universal-axis dep-graph surface. Returns `&[Dep]`
1981 /// (not `&Vec<Dep>`) because every downstream consumer of the
1982 /// runtime-dep list treats it as a read-only sequence — the slice-
1983 /// view is the narrowest borrow that supports every present +
1984 /// roadmapped consumer (`.iter()`, `.len()`, `.is_empty()`) without
1985 /// leaking the backing `Vec`'s grow/push/reserve surface no consumer
1986 /// of the typed view reaches for (the storage-side `Vec` remains
1987 /// reachable through the `pub deps` field for the mutation-carrying
1988 /// serde round-trip and per-test fixture-mutation paths). Named
1989 /// `deps()` to match the storage field's name; the accessor's
1990 /// identity maps onto the canonical CAIXA-SDLC §I vocabulary the
1991 /// slot's docstring already carries.
1992 #[must_use]
1993 pub fn deps(&self) -> &[Dep] {
1994 self.deps.as_slice()
1995 }
1996
1997 /// Substrate-canonical per-`Caixa` `:deps-dev` universal-axis
1998 /// development-only-dependency-declaration-list slice-accessor every
1999 /// consumer of the top-level manifest's dev-dep-graph axis keys off —
2000 /// returns the author-declared `:deps-dev` list verbatim as a `&[Dep]`
2001 /// slice-view over the same backing buffer the raw
2002 /// `self.deps_dev.as_slice()` field access borrows from. Empty-list-
2003 /// carrying (`:deps-dev` is a default-empty axis every `defcaixa`
2004 /// form supplies with an empty `()` when unset; the
2005 /// [`Self::from_lisp`] derive folds an omitted `:deps-dev` through
2006 /// `#[serde(default)]` to `Vec::new()`, so a `Caixa` past parse
2007 /// definitionally carries a `Vec<Dep>` slot — possibly empty — and
2008 /// the returned `&[Dep]` degenerates to an empty slice on that arm
2009 /// without any silent `None` collapse).
2010 ///
2011 /// The `:deps-dev` slot carries the universal-axis dev-only
2012 /// dependency list every kind of caixa emits under (CAIXA-SDLC §I —
2013 /// the author-facing sibling of `:deps` that every `defcaixa` form
2014 /// supplies to declare tests / lint / bench closures the runtime
2015 /// `:deps` axis does not carry; the substrate-wide dev-closure-input
2016 /// axis every downstream test-facing artifact emits under, matching
2017 /// Cargo's `[dev-dependencies]` table's dev-time-only visibility
2018 /// contract) — the typed slot's `Vec<Dep>` accept-set (empty-`:nome`
2019 /// rejected through [`DepError::NomeEmpty`], non-DNS-1123-label
2020 /// `:nome` rejected through [`DepError::NomeInvalid`], malformed
2021 /// `:versao` rejected through [`DepError::VersaoInvalid`], empty
2022 /// `:fonte.repo` rejected through [`DepError::FonteRepoEmpty`],
2023 /// within-list duplicate `:nome` rejected through
2024 /// [`DepError::DuplicateNome { list: ":deps-dev" }`]) maps onto every
2025 /// load-bearing downstream consumer the substrate carries — the
2026 /// [`Self::validate_deps`] per-entry [`Dep::validate`] + within-list
2027 /// dedup walk at caixa-core/src/manifest.rs, the
2028 /// [`crate::dep::validate_no_self_dep`] cross-list self-reference
2029 /// gate at caixa-core/src/layout.rs that checks each entry against
2030 /// the caixa's own `:nome`, the caixa-resolver
2031 /// `for dep in &root.deps_dev` closure walk at
2032 /// caixa-resolver/src/resolve.rs that seeds every dev-only git-clone
2033 /// target through the resolver's [`crate::Dep`]-keyed pipeline, and
2034 /// every future per-`Caixa` resolver-facing renderer the CAIXA-SDLC
2035 /// §I roadmap acknowledges (the future per-cluster dev-closure-audit
2036 /// overlay the M4 CR materializer resolves per-CR, the future
2037 /// `lacre.lisp` BLAKE3-closure emit walk the caixa-resolver docstring
2038 /// roadmaps).
2039 ///
2040 /// Second outer top-level [`Caixa`] `&[Dep]`-return slice-accessor —
2041 /// closes the outer-`Caixa` dependency-slot `&[Dep]` sub-family the
2042 /// sibling [`Self::deps`] (ad34b4e) opened on. The two accessors
2043 /// jointly close the two-list dep-graph surface every downstream
2044 /// resolver-facing consumer keys off (runtime `:deps` +
2045 /// dev-only `:deps-dev`, the canonical Cargo-shaped dependency-table
2046 /// pair the [`Self::validate_deps`] gate already walks in canonical
2047 /// order). Peer of the closed outer-`Caixa` foreign-code-slot
2048 /// `&[String]` sub-family ([`Self::bibliotecas`] 8a36c23,
2049 /// [`Self::exe`] 65d9527, [`Self::servicos`] 611f78b) and the outer-
2050 /// `Caixa` universal-axis text-tag family ([`Self::autores`]
2051 /// b5d813f, [`Self::etiquetas`] 78c7d3c) — folds the "outer
2052 /// [`Caixa`] `&[T]` slice" projection pattern onto the sibling
2053 /// dev-dep composite-element axis (`Dep` composite, matching the
2054 /// [`Self::deps`] element type). Sibling in shape to the peer
2055 /// per-`:supervisor` [`crate::supervisor::SupervisorSpec::children`]
2056 /// (bc92bce), per-`:placement`
2057 /// [`crate::aplicacao::Placement::clusters`] (a6e18d7),
2058 /// per-`:membros` [`crate::aplicacao::AplicacaoSpec::membros`]
2059 /// (6c77e36), per-`:contratos`
2060 /// [`crate::aplicacao::AplicacaoSpec::contratos`] (0dcc926), and
2061 /// per-`:upgrade-from :instructions`
2062 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
2063 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
2064 /// typed-slot list axes, folded here to the outer top-level
2065 /// [`Caixa`] universal-axis dev-dep-graph surface. Returns `&[Dep]`
2066 /// (not `&Vec<Dep>`) because every downstream consumer of the
2067 /// dev-dep list treats it as a read-only sequence — the slice-view
2068 /// is the narrowest borrow that supports every present +
2069 /// roadmapped consumer (`.iter()`, `.len()`, `.is_empty()`) without
2070 /// leaking the backing `Vec`'s grow/push/reserve surface no consumer
2071 /// of the typed view reaches for (the storage-side `Vec` remains
2072 /// reachable through the `pub deps_dev` field for the mutation-
2073 /// carrying serde round-trip and per-test fixture-mutation paths).
2074 /// Named `deps_dev()` to match the storage field's `snake_case` name;
2075 /// the kebab-case author-surface tag `:deps-dev` is the same axis
2076 /// after tatara-lisp's kebab↔snake fold and the accessor's identity
2077 /// maps onto the canonical CAIXA-SDLC §I vocabulary the slot's
2078 /// docstring already carries.
2079 #[must_use]
2080 pub fn deps_dev(&self) -> &[Dep] {
2081 self.deps_dev.as_slice()
2082 }
2083
2084 /// Substrate-canonical per-[`Caixa`] typed-dispatch read accessor
2085 /// every consumer that walks one of the two dep-list axes keyed on a
2086 /// [`crate::dep::DepList`] discriminant reaches for — routes the
2087 /// `(list: DepList) -> &[Dep]` projection through one typed method on
2088 /// the substrate primitive rather than the prior open-coded
2089 /// `match list { Prod => caixa.deps(), Dev => caixa.deps_dev() }`
2090 /// inline dispatch every per-axis walker would otherwise carry.
2091 /// Returns the author-declared per-list `Vec<Dep>` verbatim as a
2092 /// `&[Dep]` slice-view over the same backing buffer the sibling
2093 /// [`Self::deps`] (`Prod`) / [`Self::deps_dev`] (`Dev`) per-slot
2094 /// accessors borrow from, preserving the empty-list-carrying invariant
2095 /// each per-slot accessor already establishes (`:deps` / `:deps-dev`
2096 /// are default-empty axes every `defcaixa` form supplies with an empty
2097 /// `()` when unset; the [`Self::from_lisp`] derive folds an omitted
2098 /// list through `#[serde(default)]` to `Vec::new()`, so both arms
2099 /// definitionally carry a `Vec<Dep>` slot — possibly empty — and the
2100 /// returned `&[Dep]` degenerates to an empty slice on either arm
2101 /// without any silent `None` collapse).
2102 ///
2103 /// The [`crate::dep::DepList`] closed-set typed enum is the
2104 /// substrate's canonical discriminator for the "runtime-closure
2105 /// `:deps` vs dev-only-closure `:deps-dev`" axis every dep-list
2106 /// consumer dispatches on — the compiler-checked exhaustiveness on
2107 /// the enum's `match` arms is the build-time guarantee that no future
2108 /// per-list read-site regresses to a bare-`bool`-flag inline dispatch
2109 /// that a future third dep-list axis (a `:deps-build` build-only
2110 /// closure once the substrate grows cross-artifact heterogeneous
2111 /// dep-graphs, per CAIXA-SDLC §I) would silently split at every
2112 /// consumer. Prior to this the read side carried two per-slot
2113 /// accessors ([`Self::deps`] ad34b4e, [`Self::deps_dev`]) and no
2114 /// typed dispatch that a per-axis walker could parametrise on, so
2115 /// every per-list walker (the [`Self::validate_deps`] per-list
2116 /// [`crate::render::insert_first_seen`] dedup walk, a future
2117 /// `feira app graph` per-list dep summary, a future M4 per-cluster
2118 /// dev-closure-audit overlay the CR materializer resolves per-CR)
2119 /// open-coded the same two-block "run over `:deps`, then run over
2120 /// `:deps-dev`" pattern — a silent duplication that a future third
2121 /// dep-list axis would have had to grow a third block at every site.
2122 ///
2123 /// Peer of the sibling [`Self::push_dep`] typed-mutation dispatch
2124 /// (359fba5) — closes the two-side dispatch symmetry on the outer
2125 /// [`Caixa`] two-list dep-graph surface: `push_dep` on the mutation
2126 /// side, `deps_of` on the read side, both keyed on the same
2127 /// [`crate::dep::DepList`] discriminator. Same "one typed dispatch on
2128 /// the substrate primitive, thin projections at each consumer"
2129 /// discipline the sibling per-slot read accessors ([`Self::nome`]
2130 /// e6b7d97, [`Self::versao`], [`Self::kind`]) carry — extended onto
2131 /// the outer-[`Caixa`] typed-dispatch read surface.
2132 #[must_use]
2133 pub fn deps_of(&self, list: crate::dep::DepList) -> &[Dep] {
2134 match list {
2135 crate::dep::DepList::Prod => self.deps(),
2136 crate::dep::DepList::Dev => self.deps_dev(),
2137 }
2138 }
2139
2140 /// Substrate-canonical per-[`Caixa`] typed-mutation dispatch every
2141 /// consumer that appends to one of the two dep-list axes keys off
2142 /// — routes the `(list: DepList, dep: Dep)` tuple through one typed
2143 /// method on the substrate primitive rather than the prior
2144 /// `feira add`-side open-coded `if self.dev { &mut caixa.deps_dev }
2145 /// else { &mut caixa.deps }` inline dispatch + open-coded
2146 /// `.iter().any(|d| d.nome == …)` dup-check cascade. Refuses the
2147 /// mutation with the canonical typed [`DepError::DuplicateNome`] on
2148 /// a within-list name collision — the same `list: &'static str`
2149 /// diagnostic shape [`Self::validate_deps`]'s per-list
2150 /// [`crate::render::insert_first_seen`] walk raises on the peer
2151 /// parse-time within-list dedup axis, so a future author reading a
2152 /// `feira add` refusal and a `feira build` refusal reaches for the
2153 /// same corrective surface without switching diagnostic idioms.
2154 ///
2155 /// The two-arm [`crate::dep::DepList`] enum is the substrate's
2156 /// closed-set typed carrier for the "runtime-closure `:deps` vs
2157 /// dev-only-closure `:deps-dev`" axis every dep-list consumer
2158 /// dispatches on — the compiler-checked exhaustiveness on the
2159 /// enum's `match` arms is the build-time guarantee that no future
2160 /// per-list mutation-site regresses to a bare-`bool`-flag
2161 /// (`is_dev: bool`) inline dispatch that a future third
2162 /// dep-list axis (a `:deps-build` build-only closure once the
2163 /// substrate grows cross-artifact heterogeneous dep-graphs, per
2164 /// CAIXA-SDLC §I) would silently split at every consumer.
2165 ///
2166 /// Same "one typed dispatch on the substrate primitive, thin
2167 /// projections at each consumer" discipline the sibling per-slot
2168 /// read accessors ([`Self::deps`] ad34b4e, [`Self::deps_dev`],
2169 /// [`Self::nome`] e6b7d97, [`Self::versao`], [`Self::kind`])
2170 /// carry — extended onto the outer-[`Caixa`] typed-mutation surface,
2171 /// the substrate's first typed-mutation dispatch on the top-level
2172 /// manifest. The prior `feira add` open-coded `&mut caixa.deps` /
2173 /// `&mut caixa.deps_dev` inline field-access + `bail!` string-
2174 /// diagnostic path routed no through-line back to the typed slot,
2175 /// so a future extension of either dep-list axis to a richer author
2176 /// surface (a per-cluster override the operator pins through a
2177 /// future `:placement`-scoped dep-list slot the CAIXA-SDLC §I
2178 /// roadmap acknowledges, an M4
2179 /// `mesh.pleme.io/v1alpha1/Caixa` CR materializer's per-CR
2180 /// admission-webhook that normalized the list at admission time)
2181 /// would have had to be threaded through the `feira add` mutation
2182 /// site in lockstep with every read consumer or one path would
2183 /// silently disagree with the other on which list a given dep lands
2184 /// in. Lifting the resolution rule to a typed method on the
2185 /// substrate primitive means every downstream dep-list-mutating
2186 /// consumer of the top-level manifest reaches for exactly one typed
2187 /// dispatch — the resolver's accept-set migrates as a unit on any
2188 /// future axis addition.
2189 ///
2190 /// # Errors
2191 ///
2192 /// Returns [`DepError::DuplicateNome`] with `list = list.as_str()`
2193 /// when another entry in the same list already carries the same
2194 /// `:nome` — the mutation is refused and the caller can surface the
2195 /// typed diagnostic to the author (the `feira add` verb routes the
2196 /// error through `anyhow::Error::from`, which preserves the
2197 /// canonical `#[error(...)]`-templated diagnostic body).
2198 pub fn push_dep(&mut self, list: crate::dep::DepList, dep: Dep) -> Result<(), DepError> {
2199 let target = match list {
2200 crate::dep::DepList::Prod => &mut self.deps,
2201 crate::dep::DepList::Dev => &mut self.deps_dev,
2202 };
2203 if target.iter().any(|d| d.nome() == dep.nome()) {
2204 return Err(DepError::DuplicateNome {
2205 nome: dep.nome().to_string(),
2206 list: list.as_str(),
2207 });
2208 }
2209 target.push(dep);
2210 Ok(())
2211 }
2212
2213 /// Substrate-canonical per-`Caixa` `:limits` M2 typed-slot outer-
2214 /// composite Lunatic-per-process wasm32-sandboxing-composite optional-
2215 /// composite-reference accessor every consumer of the top-level
2216 /// manifest's per-Servico [`LimitsSpec`] outer-composite reader keys
2217 /// off — returns the author-declared `:limits` typed composite
2218 /// verbatim as an `Option<&LimitsSpec>` reference over the same
2219 /// backing storage the raw `self.limits.as_ref()` field access
2220 /// borrows from, with `None` naming the "no `:limits` block
2221 /// authored — every per-axis Lunatic-sandbox cap defers to the
2222 /// wasm-engine-default arm named on the per-axis
2223 /// [`LimitsSpec::memory`] / [`LimitsSpec::fuel`] /
2224 /// [`LimitsSpec::wall_clock`] / [`LimitsSpec::cpu`] scalar-accessor
2225 /// docstrings" partition every downstream Servico-M2-overlay
2226 /// emitter treats as "emit nothing" and the sibling
2227 /// [`crate::StandardLayout::verify`] per-`:limits` shape gate
2228 /// treats as "skip the per-axis
2229 /// [`crate::LimitsError::MemoryZero`] / `MemoryBelowWasm32Page` /
2230 /// `FuelZero` / `WallClockZero` / `CpuZero` refusal cascade".
2231 ///
2232 /// The outer `:limits` slot carries the M2 Servico-runtime typed
2233 /// composite — the load-bearing container of every Lunatic-shaped
2234 /// per-process wasm32-sandbox cap axis every long-running wasm
2235 /// component's runtime dispatches on (INSPIRATIONS §III.1 —
2236 /// Lunatic per-process linear-memory / fuel / wall-clock /
2237 /// millicore cap primitives translated onto pleme-io's typed
2238 /// `:limits :memory` / `:limits :fuel` / `:limits :wall-clock` /
2239 /// `:limits :cpu` sub-slot axes; CAIXA-SDLC §II — the typed-M2
2240 /// slot algebra the wasm-engine + `pleme-computeunit` Helm-library
2241 /// chart both fan on). Every per-`:limits` axis threads through a
2242 /// lifted per-slot accessor on the [`LimitsSpec`] type: the
2243 /// [`LimitsSpec::memory`] wasm32 linear-memory byte-cap scalar
2244 /// accessor, the [`LimitsSpec::fuel`] wasmtime fuel-cap scalar
2245 /// accessor, the [`LimitsSpec::wall_clock`] per-call wall-clock
2246 /// deadline scalar accessor, and the [`LimitsSpec::cpu`]
2247 /// K8s-millicore soft-CPU-share scalar accessor. Every downstream
2248 /// consumer that reaches for a limits axis first passes through
2249 /// this outer accessor onto the composite and then dispatches
2250 /// onto the per-axis accessor — the two-level dispatch means
2251 /// every per-`:limits` reader now routes through a typed dispatch
2252 /// on the substrate primitive at both altitudes.
2253 ///
2254 /// Prior to this lift the `.limits` `Option<LimitsSpec>` composite
2255 /// was accessed inline at three production sites — the
2256 /// [`crate::StandardLayout::verify`] per-`:limits` shape gate's
2257 /// `if let Some(l) = &caixa.limits { … }` traversal head
2258 /// (caixa-core/src/layout.rs:882, which drives the per-axis
2259 /// refusal cascade on the composite: the `LimitsError::MemoryZero`
2260 /// / `MemoryBelowWasm32Page` / `MemoryExceedsWasm32Max` /
2261 /// `FuelZero` / `FuelExceedsMax` / `WallClockZero` /
2262 /// `WallClockExceedsMax` / `CpuZero` / `CpuExceedsMax` refusals
2263 /// [`LimitsSpec::validate`] fans onto), the
2264 /// [`crate::render::servico_m2_overlay`] per-Servico M2 overlay
2265 /// emitter's `if let Some(limits) = &caixa.limits { … }` traversal
2266 /// head (caixa-core/src/render.rs:18504, which drives the
2267 /// `M2_KEY_LIMITS`-keyed `limits.is_empty()`-gated `serde_yaml`
2268 /// projection every `caixa-helm` / `caixa-flux` Servico values-
2269 /// block emitter fans on), and the
2270 /// [`Self::declared_servico_slots`] per-Servico M2 declared-slot-
2271 /// set enumerator's `self.limits.is_some()` presence probe
2272 /// (caixa-core/src/manifest.rs:1788, which drives the
2273 /// `M2_AUTHOR_KEY_LIMITS` kebab-case author-label push every
2274 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] kind-coherence
2275 /// gate reads) — three open-coded outer-field accesses that
2276 /// expressed no compile-time link back to the typed slot at the
2277 /// [`Caixa`] altitude. A future extension of the `:limits` outer
2278 /// axis to a richer author surface (a multi-`:limits` list the M4
2279 /// CR materializer resolves per-CR at admission time so a Servico
2280 /// can expose a compute-heavy + IO-heavy limits pair, a per-
2281 /// cluster `:limits-overrides` slot the operator pins so a
2282 /// cluster-specific policy can tighten a caixa-declared cap
2283 /// without re-authoring the `caixa.lisp`, a promotion of the
2284 /// plain `Option<LimitsSpec>` to a richer
2285 /// `{static, dynamic}` partition once the wasm-engine's runtime-
2286 /// resolved dynamic-cap surface lands) would have had to be
2287 /// threaded through all three open-coded copies in lockstep or
2288 /// one consumer would silently disagree with the peers on which
2289 /// limits composite a given Caixa resolves to — the layout gate's
2290 /// per-axis bracket-dispatch seed reading the raw slot while the
2291 /// peer `servico_m2_overlay` emitter read an operator-resolved
2292 /// slot would silently split the build-time sandbox-shape gate
2293 /// from the runtime `ComputeUnit` CR emission gate, a three-
2294 /// consumer split at the layout gate, the M2 overlay emitter, and
2295 /// the declared-slot enumerator far from the source `caixa.lisp`
2296 /// with no field naming the limits-drift root cause. Lifting the
2297 /// resolution rule to a typed method on the substrate primitive
2298 /// means every downstream consumer of the caixa's per-`Caixa`
2299 /// Lunatic-sandboxing outer-composite surface reaches for exactly
2300 /// one typed dispatch — the resolver's accept-set migrates as a
2301 /// unit on any future axis addition.
2302 ///
2303 /// First outer top-level [`Caixa`] `Option<&Composite>`-return
2304 /// composite-reference accessor — opens the outer-`Caixa`
2305 /// `Option<&Composite>` composite-reference projection pattern the
2306 /// sibling per-`Caixa` `:behavior` [`crate::BehaviorSpec`] /
2307 /// `:politicas` [`crate::aplicacao::MeshPolicy`] / `:placement`
2308 /// [`crate::aplicacao::Placement`] / `:entrada`
2309 /// [`crate::aplicacao::Entrada`] future outer-composite lifts
2310 /// fold on. Peer of the M3 mesh-slot outer-composite family the
2311 /// sibling [`crate::AplicacaoSpec::politicas`] (534dc21) /
2312 /// [`crate::AplicacaoSpec::placement`] (9abb8f0) /
2313 /// [`crate::AplicacaoSpec::entrada`] (d32111c) composite-reference
2314 /// accessors already close on the outer [`crate::AplicacaoSpec`]
2315 /// altitude — extends that "one typed dispatch on the substrate
2316 /// primitive, thin projections at each consumer" discipline onto
2317 /// the outer top-level [`Caixa`] altitude, opening the M2 Servico-
2318 /// runtime slot family's outer-composite axis. Returns
2319 /// `Option<&LimitsSpec>` (not the owning composite by copy or
2320 /// clone) because every downstream consumer of the limits
2321 /// composite treats it as a read-only per-axis dispatch source —
2322 /// the reference-view is the narrowest borrow that supports every
2323 /// present + roadmapped consumer (per-axis accessor dispatch,
2324 /// `.is_empty()`-gated overlay projection, presence-probe early
2325 /// return on the "author-omitted `:limits` ⇒ engine-default
2326 /// applies" partition) without cloning the composite through
2327 /// every consumer's fast path. The `Option` half of the return-
2328 /// type preserves the load-bearing "author-omitted `:limits` ⇒
2329 /// engine-default applies" partition (not a default composite the
2330 /// downstream must reject on emptiness) — the accessor projects
2331 /// the raw `Option<LimitsSpec>` slot's presence bit through the
2332 /// reference-return unchanged. Named `limits()` to match the
2333 /// storage field's name verbatim and the tatara-lisp author-
2334 /// surface term (`:limits`) the field's own docstring already
2335 /// carries.
2336 #[must_use]
2337 pub fn limits(&self) -> Option<&LimitsSpec> {
2338 self.limits.as_ref()
2339 }
2340
2341 /// Substrate-canonical per-`Caixa` `:behavior` M2 typed-slot outer-
2342 /// composite OTP-`gen_server`-shaped callback-table optional-
2343 /// composite-reference accessor every consumer of the top-level
2344 /// manifest's per-Servico [`BehaviorSpec`] outer-composite reader
2345 /// keys off — returns the author-declared `:behavior` typed
2346 /// composite verbatim as an `Option<&BehaviorSpec>` reference over
2347 /// the same backing storage the raw `self.behavior.as_ref()` field
2348 /// access borrows from, with `None` naming the "no `:behavior`
2349 /// block authored — every per-callback OTP-shaped hook defers to
2350 /// the wasm-engine's runtime default arm named on the per-axis
2351 /// [`BehaviorSpec::on_init`] / [`BehaviorSpec::on_call`] /
2352 /// [`BehaviorSpec::on_cast`] / [`BehaviorSpec::on_info`] /
2353 /// [`BehaviorSpec::on_state_change`] /
2354 /// [`BehaviorSpec::on_terminate`] scalar-accessor docstrings"
2355 /// partition every downstream Servico-M2-overlay emitter treats as
2356 /// "emit nothing" and the sibling [`crate::StandardLayout::verify`]
2357 /// per-`:behavior` shape gate treats as "skip the per-arm
2358 /// [`crate::behavior::BehaviorError`] refusal cascade + the
2359 /// per-callback on-disk `MissingEntry` existence check".
2360 ///
2361 /// The outer `:behavior` slot carries the M2 Servico-runtime typed
2362 /// composite — the load-bearing container of every OTP-shaped
2363 /// per-Servico lifecycle-callback path axis every long-running wasm
2364 /// component's runtime dispatches on (INSPIRATIONS §II.3 — Erlang/
2365 /// OTP `gen_server:init/1` / `handle_call/3` / `handle_cast/2` /
2366 /// `handle_info/2` / `code_change/3` / `terminate/2` primitives
2367 /// translated onto pleme-io's typed `:behavior :on-init` /
2368 /// `:on-call` / `:on-cast` / `:on-info` / `:on-state-change` /
2369 /// `:on-terminate` sub-slot axes; CAIXA-SDLC §II — the typed-M2
2370 /// slot algebra the wasm-engine + `pleme-computeunit` Helm-library
2371 /// chart both fan on). Every per-`:behavior` axis threads through a
2372 /// lifted per-callback accessor on the [`BehaviorSpec`] type
2373 /// (9b4ecde / d66c702 / 156ddbe / 99616ac / 4846cef / 701add7).
2374 /// Every downstream consumer that reaches for a behavior axis
2375 /// first passes through this outer accessor onto the composite
2376 /// and then dispatches onto the per-callback accessor — the
2377 /// two-level dispatch means every per-`:behavior` reader now
2378 /// routes through a typed dispatch on the substrate primitive at
2379 /// both altitudes.
2380 ///
2381 /// Composes cross-slot with the M2 `:upgrade-from` gate: the
2382 /// [`crate::upgrade::validate_upgrade_from_against_behavior`]
2383 /// cross-slot composition gate at [`crate::StandardLayout::verify`]
2384 /// keys the "per-version `:state-change` instruction must have a
2385 /// `:on-state-change` callback" precondition off this accessor's
2386 /// composite (the callback-side counterpart to the
2387 /// `:upgrade-from :instructions :state-change :script` refusal at
2388 /// the appup-side). Threading that gate's traversal input through
2389 /// this accessor closes the cross-slot invariant on the substrate
2390 /// primitive, not on the raw field.
2391 ///
2392 /// Prior to this lift the `.behavior` `Option<BehaviorSpec>`
2393 /// composite was accessed inline at four production sites — the
2394 /// [`crate::StandardLayout::verify`] per-`:behavior` shape gate's
2395 /// `if let Some(b) = &caixa.behavior { … }` traversal head
2396 /// (caixa-core/src/layout.rs:896, which drives the per-arm
2397 /// `BehaviorError` refusal cascade + the per-callback on-disk
2398 /// [`crate::LayoutError::MissingEntry`] existence check under
2399 /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK`]),
2400 /// the [`crate::upgrade::validate_upgrade_from_against_behavior`]
2401 /// cross-slot composition gate's `caixa.behavior.as_ref()`
2402 /// traversal-input feed (caixa-core/src/layout.rs:1008, which
2403 /// drives the `:state-change` ↔ `:on-state-change` precondition
2404 /// refusal), the [`crate::render::servico_m2_overlay`] per-Servico
2405 /// M2 overlay emitter's `if let Some(behavior) = &caixa.behavior
2406 /// { … }` traversal head (caixa-core/src/render.rs:18513, which
2407 /// drives the `M2_KEY_BEHAVIOR`-keyed `behavior.is_empty()`-gated
2408 /// `serde_yaml` projection every `caixa-helm` / `caixa-flux`
2409 /// Servico values-block emitter fans on), and the
2410 /// [`Self::declared_servico_slots`] per-Servico M2 declared-slot-
2411 /// set enumerator's `self.behavior.is_some()` presence probe
2412 /// (caixa-core/src/manifest.rs:1919, which drives the
2413 /// `M2_AUTHOR_KEY_BEHAVIOR` kebab-case author-label push every
2414 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] kind-coherence
2415 /// gate reads) — four open-coded outer-field accesses that
2416 /// expressed no compile-time link back to the typed slot at the
2417 /// [`Caixa`] altitude. A future extension of the `:behavior`
2418 /// outer axis to a richer author surface (a per-callback overlay
2419 /// resolver the operator materializes at admission time so a
2420 /// cluster-specific policy can inject a per-callback tracing
2421 /// interceptor without re-authoring the `caixa.lisp`, a promotion
2422 /// of the plain `Option<BehaviorSpec>` to a richer `{static,
2423 /// dynamic}` partition once a runtime-resolved behavior-swap
2424 /// surface lands, the M4 per-callback middleware chain the
2425 /// caixa-operator's per-Servico admission webhook keys off) would
2426 /// have had to be threaded through all four open-coded copies in
2427 /// lockstep or one consumer would silently disagree with the
2428 /// peers on which behavior composite a given Caixa resolves to —
2429 /// the layout gate's per-callback existence-check seed reading
2430 /// the raw slot while the peer `servico_m2_overlay` emitter read
2431 /// an operator-resolved slot would silently split the build-time
2432 /// callback-shape gate from the runtime `ComputeUnit` CR emission
2433 /// gate from the cross-slot `:state-change` composition gate from
2434 /// the M2 declared-slot enumerator, a four-consumer split far
2435 /// from the source `caixa.lisp` with no field naming the
2436 /// behavior-drift root cause. Lifting the resolution rule to a
2437 /// typed method on the substrate primitive means every downstream
2438 /// consumer of the caixa's per-`Caixa` OTP-callback-table outer-
2439 /// composite surface reaches for exactly one typed dispatch — the
2440 /// resolver's accept-set migrates as a unit on any future axis
2441 /// addition.
2442 ///
2443 /// Second outer top-level [`Caixa`] `Option<&Composite>`-return
2444 /// composite-reference accessor — sibling to the opening
2445 /// [`Self::limits`] (b2bd9d7) accessor on the outer-`Caixa`
2446 /// `Option<&Composite>` composite-reference sub-family, extends
2447 /// the "one typed dispatch on the substrate primitive, thin
2448 /// projections at each consumer" discipline onto the second of
2449 /// the three M2 Servico-runtime slots. The remaining
2450 /// `Option<&Composite>` axes at the outer top-level [`Caixa`]
2451 /// altitude — the M3 mesh-slot family (`:politicas`,
2452 /// `:placement`, `:entrada` — already closed on the inner
2453 /// [`crate::AplicacaoSpec`] altitude via 534dc21 / 9abb8f0 /
2454 /// d32111c) — remain the future sibling lifts on the outer
2455 /// top-level projection. Returns `Option<&BehaviorSpec>` (not
2456 /// the owning composite by copy or clone) because every
2457 /// downstream consumer of the behavior composite treats it as a
2458 /// read-only per-callback dispatch source — the reference-view is
2459 /// the narrowest borrow that supports every present + roadmapped
2460 /// consumer (per-callback accessor dispatch, `.is_empty()`-gated
2461 /// overlay projection, presence-probe early return on the
2462 /// "author-omitted `:behavior` ⇒ runtime-default applies"
2463 /// partition, cross-slot `:state-change` composition input)
2464 /// without cloning the composite through every consumer's fast
2465 /// path. The `Option` half of the return-type preserves the
2466 /// load-bearing "author-omitted `:behavior` ⇒ runtime-default
2467 /// applies" partition (not a default composite the downstream
2468 /// must reject on emptiness) — the accessor projects the raw
2469 /// `Option<BehaviorSpec>` slot's presence bit through the
2470 /// reference-return unchanged. Named `behavior()` to match the
2471 /// storage field's name verbatim and the tatara-lisp author-
2472 /// surface term (`:behavior`) the field's own docstring already
2473 /// carries.
2474 #[must_use]
2475 pub fn behavior(&self) -> Option<&crate::BehaviorSpec> {
2476 self.behavior.as_ref()
2477 }
2478
2479 /// Substrate-canonical per-`Caixa` `:politicas` M3 mesh-slot outer-
2480 /// composite MESH-COMPOSITION-shaped mesh-policy optional-composite-
2481 /// reference accessor every consumer of the top-level manifest's
2482 /// per-Aplicacao [`crate::aplicacao::MeshPolicy`] outer-composite
2483 /// reader keys off — returns the author-declared `:politicas` typed
2484 /// composite verbatim as an `Option<&MeshPolicy>` reference over the
2485 /// same backing storage the raw `self.politicas.as_ref()` field
2486 /// access borrows from, with `None` naming the "no `:politicas`
2487 /// block authored — every per-axis mesh-policy scalar defers to the
2488 /// cluster-default arm named on the per-axis
2489 /// [`crate::aplicacao::MeshPolicy::timeout`] /
2490 /// [`crate::aplicacao::MeshPolicy::retries`] /
2491 /// [`crate::aplicacao::MeshPolicy::circuit_breaker`] /
2492 /// [`crate::aplicacao::MeshPolicy::mtls_required`] /
2493 /// [`crate::aplicacao::MeshPolicy::rate_limit`] scalar-accessor
2494 /// docstrings" partition every downstream caixa-mesh /
2495 /// caixa-flux / caixa-helm Aplicacao-artifact emitter treats as
2496 /// "emit no per-`:politicas` overlay" and the sibling
2497 /// [`Self::aplicacao_view`] Aplicacao-composition seed folds through
2498 /// the [`crate::aplicacao::MeshPolicy::default`] cluster-default
2499 /// arm.
2500 ///
2501 /// The outer `:politicas` slot carries the M3 mesh-slot per-
2502 /// Aplicacao typed composite — the load-bearing container of every
2503 /// mesh-level policy axis every Cilium NetworkPolicy / Gateway API
2504 /// v1.x HTTPRoute / future M4 per-edge policy overlay emitter fans
2505 /// on (MESH-COMPOSITION §III.2 — the Aplicacao's typed mesh-policy
2506 /// composite; §V — the "no infinite blocking" per-call deadline +
2507 /// "sandboxing-by-default" mTLS-enforcement CSE invariants; §III.3
2508 /// — the typed inter-Servico contrato-edge overlay the per-`(:de,
2509 /// :para)` mesh renderer keys off). Every per-`:politicas` axis
2510 /// threads through a lifted per-slot accessor on the
2511 /// [`crate::aplicacao::MeshPolicy`] type: the
2512 /// [`crate::aplicacao::MeshPolicy::mtls_required`] (c0110f1) Cilium
2513 /// mTLS-enforcement toggle, the
2514 /// [`crate::aplicacao::MeshPolicy::retries`] (bdfb399) transient-
2515 /// failure retry budget, the [`crate::aplicacao::MeshPolicy::timeout`]
2516 /// (7073d0f) Gateway-API per-call deadline, the
2517 /// [`crate::aplicacao::MeshPolicy::circuit_breaker`] (b0e741a)
2518 /// Envoy-outlier-detection composite. Every downstream consumer
2519 /// that reaches for a mesh-policy axis first passes through this
2520 /// outer accessor onto the composite and then dispatches onto the
2521 /// per-axis accessor — the two-level dispatch means every per-
2522 /// `:politicas` reader now routes through a typed dispatch on the
2523 /// substrate primitive at both altitudes.
2524 ///
2525 /// Composes through [`Self::aplicacao_view`]'s Aplicacao-composition
2526 /// seed: the Aplicacao-view builder folds the outer `Option`'s
2527 /// author-omitted arm onto the [`crate::aplicacao::MeshPolicy::default`]
2528 /// cluster-default, so the peer inner [`crate::AplicacaoSpec::politicas`]
2529 /// (534dc21) `&MeshPolicy`-return accessor observes a typed
2530 /// composite whether or not the author declared the outer slot.
2531 /// The outer accessor preserves the "author-omitted vs authored-
2532 /// empty" partition the inner accessor's `is_empty()`-gated
2533 /// renderer overlay collapses — routing the presence bit through
2534 /// this accessor keeps the [`Self::declared_mesh_slots`] M3 kind-
2535 /// coherence enumerator's `M3_AUTHOR_KEY_POLITICAS` push separate
2536 /// from the inner `MeshPolicy::is_empty()`-gated overlay elision.
2537 ///
2538 /// Prior to this lift the `.politicas` `Option<MeshPolicy>`
2539 /// composite was accessed inline at two production sites — the
2540 /// [`Self::aplicacao_view`] Aplicacao-composition seed's
2541 /// `self.politicas.clone().unwrap_or_default()` traversal head
2542 /// (caixa-core/src/manifest.rs:1899, which drives the fold onto
2543 /// the [`crate::aplicacao::MeshPolicy::default`] cluster-default
2544 /// arm the inner [`crate::AplicacaoSpec::politicas`] accessor
2545 /// then observes), and the [`Self::declared_mesh_slots`] M3
2546 /// declared-slot-set enumerator's `self.politicas.is_some()`
2547 /// presence probe (caixa-core/src/manifest.rs:1961, which drives
2548 /// the `M3_AUTHOR_KEY_POLITICAS` kebab-case author-label push
2549 /// every [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
2550 /// coherence gate reads) — two open-coded outer-field accesses
2551 /// that expressed no compile-time link back to the typed slot at
2552 /// the [`Caixa`] altitude. A future extension of the `:politicas`
2553 /// outer axis to a richer author surface (a per-cluster
2554 /// `:politicas-overrides` slot the operator materializes at
2555 /// admission time so a cluster-specific policy can tighten the
2556 /// caixa-declared bound without re-authoring the `caixa.lisp`, a
2557 /// promotion of the plain `Option<MeshPolicy>` to a richer
2558 /// `{static, dynamic}` partition once the M4 per-edge
2559 /// contrato-scoped policy-override surface lands, the M5 traffic-
2560 /// shaping composition the caixa-operator's per-Aplicacao mesh
2561 /// admission webhook keys off) would have had to be threaded
2562 /// through both open-coded copies in lockstep or the Aplicacao-
2563 /// composition seed's default-fold arm would silently disagree
2564 /// with the M3 declared-slot enumerator on which policy composite
2565 /// a given Caixa resolves to — the seed reading an operator-
2566 /// resolved slot while the enumerator's presence probe read the
2567 /// raw slot would silently split the build-time mesh-artifact
2568 /// emission gate from the M3 declared-slot enumerator's kind-
2569 /// coherence gate, a two-consumer split far from the source
2570 /// `caixa.lisp` with no field naming the policy-drift root cause.
2571 /// Lifting the resolution rule to a typed method on the substrate
2572 /// primitive means every downstream consumer of the caixa's per-
2573 /// `Caixa` MESH-COMPOSITION mesh-policy outer-composite surface
2574 /// reaches for exactly one typed dispatch — the resolver's
2575 /// accept-set migrates as a unit on any future axis addition.
2576 ///
2577 /// Third outer top-level [`Caixa`] `Option<&Composite>`-return
2578 /// composite-reference accessor — sibling to the opening
2579 /// [`Self::limits`] (b2bd9d7) and [`Self::behavior`] (35d8b52)
2580 /// accessors on the outer-`Caixa` `Option<&Composite>` composite-
2581 /// reference sub-family, extends the "one typed dispatch on the
2582 /// substrate primitive, thin projections at each consumer"
2583 /// discipline onto the first of the three M3 mesh-slot axes.
2584 /// Peer of the closed inner mesh-slot outer-composite family the
2585 /// sibling [`crate::AplicacaoSpec::politicas`] (534dc21) /
2586 /// [`crate::AplicacaoSpec::placement`] (9abb8f0) /
2587 /// [`crate::AplicacaoSpec::entrada`] (d32111c) composite-reference
2588 /// accessor pins already close on the inner [`crate::AplicacaoSpec`]
2589 /// altitude — opens the outer top-level [`Caixa`] altitude's M3
2590 /// mesh-slot arm of the composite-reference family the remaining
2591 /// two axes (`:placement`, `:entrada`) fold onto in future
2592 /// sibling lifts. Returns `Option<&MeshPolicy>` (not the owning
2593 /// composite by copy or clone) because every downstream consumer
2594 /// of the mesh-policy composite treats it as a read-only per-axis
2595 /// dispatch source — the reference-view is the narrowest borrow
2596 /// that supports every present + roadmapped consumer (per-axis
2597 /// accessor dispatch, `.is_empty()`-gated overlay projection,
2598 /// presence-probe early return on the "author-omitted `:politicas`
2599 /// ⇒ cluster-default applies" partition, `Aplicacao`-composition
2600 /// seed's default-fold arm) without cloning the composite through
2601 /// every consumer's fast path. The `Option` half of the return-
2602 /// type preserves the load-bearing "author-omitted `:politicas` ⇒
2603 /// cluster-default applies" partition (not a default composite
2604 /// the downstream must reject on emptiness) — the accessor
2605 /// projects the raw `Option<MeshPolicy>` slot's presence bit
2606 /// through the reference-return unchanged. Named `politicas()` to
2607 /// match the storage field's name verbatim and the tatara-lisp
2608 /// author-surface term (`:politicas`) the field's own docstring
2609 /// already carries.
2610 #[must_use]
2611 pub fn politicas(&self) -> Option<&crate::aplicacao::MeshPolicy> {
2612 self.politicas.as_ref()
2613 }
2614
2615 /// Substrate-canonical per-`Caixa` `:placement` M3 mesh-slot outer-
2616 /// composite MESH-COMPOSITION-shaped distribution optional-composite-
2617 /// reference accessor every consumer of the top-level manifest's
2618 /// per-Aplicacao [`crate::aplicacao::Placement`] outer-composite
2619 /// reader keys off — returns the author-declared `:placement` typed
2620 /// composite verbatim as an `Option<&Placement>` reference over the
2621 /// same backing storage the raw `self.placement.as_ref()` field
2622 /// access borrows from, with `None` naming the "no `:placement`
2623 /// block authored — every per-axis placement scalar defers to the
2624 /// cluster-default arm named on the per-axis
2625 /// [`crate::aplicacao::Placement::estrategia`] /
2626 /// [`crate::aplicacao::Placement::clusters`] /
2627 /// [`crate::aplicacao::Placement::affinity`] /
2628 /// [`crate::aplicacao::Placement::shard_key`] scalar-accessor
2629 /// docstrings" partition every downstream caixa-mesh /
2630 /// caixa-flux / caixa-helm Aplicacao-artifact emitter treats as
2631 /// "emit no per-`:placement` overlay" and the sibling
2632 /// [`Self::aplicacao_view`] Aplicacao-composition seed folds through
2633 /// the [`crate::aplicacao::Placement::default`] cluster-default arm.
2634 ///
2635 /// The outer `:placement` slot carries the M3 mesh-slot per-
2636 /// Aplicacao typed distribution composite — the load-bearing
2637 /// container of every where-does-this-Aplicacao-run axis every
2638 /// caixa-mesh programs.yaml per-cluster distribution overlay /
2639 /// caixa-flux per-Aplicacao GitRepository/HelmRelease fan-out /
2640 /// future M4 per-Aplicacao Akka-style cluster-sharding entity-id
2641 /// resolver emitter fans on (MESH-COMPOSITION §II.4 — the
2642 /// Aplicacao's typed distribution composite; §V CSE invariants —
2643 /// "distribution is a first-class typed composite, not a runtime
2644 /// scheduler hint" the per-axis scalars enforce; §III.3 — the
2645 /// typed inter-Servico contrato-edge overlay the per-cluster
2646 /// mesh renderer keys off). Every per-`:placement` axis threads
2647 /// through a lifted per-slot accessor on the
2648 /// [`crate::aplicacao::Placement`] type: the
2649 /// [`crate::aplicacao::Placement::estrategia`] (921fe1b)
2650 /// MESH-COMPOSITION distribution-strategy scalar, the
2651 /// [`crate::aplicacao::Placement::clusters`] (a6e18d7) per-cluster
2652 /// distribution-target slice, the [`crate::aplicacao::Placement::affinity`]
2653 /// M3-Adaptive-compression-hint optional-scalar, and the
2654 /// [`crate::aplicacao::Placement::shard_key`] (7cd2a28) Akka-cluster-
2655 /// sharding extractor-expression optional-scalar. Every downstream
2656 /// consumer that reaches for a placement axis first passes through
2657 /// this outer accessor onto the composite and then dispatches onto
2658 /// the per-axis accessor — the two-level dispatch means every per-
2659 /// `:placement` reader now routes through a typed dispatch on the
2660 /// substrate primitive at both altitudes.
2661 ///
2662 /// Composes through [`Self::aplicacao_view`]'s Aplicacao-composition
2663 /// seed: the Aplicacao-view builder folds the outer `Option`'s
2664 /// author-omitted arm onto the [`crate::aplicacao::Placement::default`]
2665 /// cluster-default, so the peer inner [`crate::AplicacaoSpec::placement`]
2666 /// (9abb8f0) `&Placement`-return accessor observes a typed composite
2667 /// whether or not the author declared the outer slot. The outer
2668 /// accessor preserves the "author-omitted vs authored-empty" partition
2669 /// the inner accessor collapses at the cluster-default fold —
2670 /// routing the presence bit through this accessor keeps the
2671 /// [`Self::declared_mesh_slots`] M3 kind-coherence enumerator's
2672 /// `M3_AUTHOR_KEY_PLACEMENT` push separate from the inner
2673 /// [`crate::AplicacaoSpec::validate_placement`]-gated overlay
2674 /// dispatch.
2675 ///
2676 /// Prior to this lift the `.placement` `Option<Placement>`
2677 /// composite was accessed inline at two production sites — the
2678 /// [`Self::aplicacao_view`] Aplicacao-composition seed's
2679 /// `self.placement.clone().unwrap_or_default()` traversal head
2680 /// (caixa-core/src/manifest.rs:2036, which drives the fold onto
2681 /// the [`crate::aplicacao::Placement::default`] cluster-default
2682 /// arm the inner [`crate::AplicacaoSpec::placement`] accessor
2683 /// then observes), and the [`Self::declared_mesh_slots`] M3
2684 /// declared-slot-set enumerator's `self.placement.is_some()`
2685 /// presence probe (caixa-core/src/manifest.rs:2100, which drives
2686 /// the `M3_AUTHOR_KEY_PLACEMENT` kebab-case author-label push
2687 /// every [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
2688 /// coherence gate reads) — two open-coded outer-field accesses
2689 /// that expressed no compile-time link back to the typed slot at
2690 /// the [`Caixa`] altitude. A future extension of the `:placement`
2691 /// outer axis to a richer author surface (a per-cluster
2692 /// `:placement-overrides` slot the operator materializes at
2693 /// admission time so a cluster-specific placement can tighten the
2694 /// caixa-declared bound without re-authoring the `caixa.lisp`, a
2695 /// per-tenant placement-alias table the M4
2696 /// `mesh.pleme.io/v1alpha1/Aplicacao` CR materializer resolves
2697 /// per-CR at admission time, a promotion of the plain
2698 /// `Option<Placement>` to a richer `{static, dynamic}` partition
2699 /// once Orleans-style virtual-actor dynamic placement comes into
2700 /// typed scope) would have had to be threaded through both open-
2701 /// coded copies in lockstep or the Aplicacao-composition seed's
2702 /// default-fold arm would silently disagree with the M3 declared-
2703 /// slot enumerator on which distribution composite a given Caixa
2704 /// resolves to — the seed reading an operator-resolved slot while
2705 /// the enumerator's presence probe read the raw slot would
2706 /// silently split the build-time distribution-artifact emission
2707 /// gate from the M3 declared-slot enumerator's kind-coherence
2708 /// gate, a two-consumer split far from the source `caixa.lisp`
2709 /// with no field naming the distribution-drift root cause.
2710 /// Lifting the resolution rule to a typed method on the substrate
2711 /// primitive means every downstream consumer of the caixa's per-
2712 /// `Caixa` MESH-COMPOSITION distribution outer-composite surface
2713 /// reaches for exactly one typed dispatch — the resolver's
2714 /// accept-set migrates as a unit on any future axis addition.
2715 ///
2716 /// Fourth outer top-level [`Caixa`] `Option<&Composite>`-return
2717 /// composite-reference accessor — sibling to the opening
2718 /// [`Self::limits`] (b2bd9d7) / [`Self::behavior`] (35d8b52) M2-
2719 /// Servico-runtime pair and the peer [`Self::politicas`] (5d23d29)
2720 /// M3-mesh-slot arm on the outer-`Caixa` `Option<&Composite>`
2721 /// composite-reference sub-family, folds on the "one typed
2722 /// dispatch on the substrate primitive, thin projections at each
2723 /// consumer" discipline extended onto the second of the three M3
2724 /// mesh-slot axes. Peer of the closed inner mesh-slot outer-
2725 /// composite family the sibling
2726 /// [`crate::AplicacaoSpec::politicas`] (534dc21) /
2727 /// [`crate::AplicacaoSpec::placement`] (9abb8f0) /
2728 /// [`crate::AplicacaoSpec::entrada`] (d32111c) composite-reference
2729 /// accessor pins already close on the inner
2730 /// [`crate::AplicacaoSpec`] altitude — folds on the outer top-
2731 /// level [`Caixa`] altitude's M3 mesh-slot arm the sibling
2732 /// [`Self::politicas`] opened, extending the discipline onto the
2733 /// second of the three M3 mesh-slot axes. The remaining M3
2734 /// mesh-slot axis (`:entrada`) folds onto this accessor's
2735 /// discipline in the final sibling lift, closing the outer top-
2736 /// level [`Caixa`] `Option<&Composite>` M3 mesh-slot sub-family.
2737 /// Returns `Option<&Placement>` (not the owning composite by copy
2738 /// or clone) because every downstream consumer of the placement
2739 /// composite treats it as a read-only per-axis dispatch source —
2740 /// the reference-view is the narrowest borrow that supports every
2741 /// present + roadmapped consumer (per-axis accessor dispatch,
2742 /// serde composite-serialization on the programs.yaml overlay,
2743 /// presence-probe early return on the "author-omitted `:placement`
2744 /// ⇒ cluster-default applies" partition, `Aplicacao`-composition
2745 /// seed's default-fold arm) without cloning the composite through
2746 /// every consumer's fast path. The `Option` half of the return-
2747 /// type preserves the load-bearing "author-omitted `:placement` ⇒
2748 /// cluster-default applies" partition (not a default composite
2749 /// the downstream must reject on emptiness) — the accessor
2750 /// projects the raw `Option<Placement>` slot's presence bit
2751 /// through the reference-return unchanged. Named `placement()` to
2752 /// match the storage field's name verbatim and the tatara-lisp
2753 /// author-surface term (`:placement`) the field's own docstring
2754 /// already carries.
2755 #[must_use]
2756 pub fn placement(&self) -> Option<&crate::aplicacao::Placement> {
2757 self.placement.as_ref()
2758 }
2759
2760 /// Substrate-canonical per-`Caixa` `:entrada` M3 mesh-slot outer-
2761 /// composite MESH-COMPOSITION-shaped external-gateway optional-
2762 /// composite-reference accessor every consumer of the top-level
2763 /// manifest's per-Aplicacao [`crate::aplicacao::Entrada`] outer-
2764 /// composite reader keys off — returns the author-declared
2765 /// `:entrada` typed composite verbatim as an `Option<&Entrada>`
2766 /// reference over the same backing storage the raw
2767 /// `self.entrada.as_ref()` field access borrows from, with `None`
2768 /// naming the "no `:entrada` block authored — this Aplicacao is
2769 /// cluster-internal, no `Gateway`/`HTTPRoute` fan-out emitted"
2770 /// partition every downstream caixa-mesh Gateway-API artifact
2771 /// emitter treats as "emit no gateway-listener + no `HTTPRoute`
2772 /// backend for this Aplicacao" and the sibling
2773 /// [`Self::aplicacao_view`] Aplicacao-composition seed forwards
2774 /// verbatim (unlike the peer `:politicas` / `:placement` arms,
2775 /// `:entrada` has no cluster-default fold — an omitted `:entrada`
2776 /// stays `None` on the projected [`crate::AplicacaoSpec`] and the
2777 /// peer inner [`crate::AplicacaoSpec::entrada`] accessor observes
2778 /// the same `Option<&Entrada>` presence bit unchanged).
2779 ///
2780 /// The outer `:entrada` slot carries the M3 mesh-slot per-
2781 /// Aplicacao typed external-gateway composite — the load-bearing
2782 /// container of every how-does-the-outside-world-reach-this-
2783 /// Aplicacao axis every caixa-mesh `Gateway`/`HTTPRoute` fan-out
2784 /// emitter fans on (MESH-COMPOSITION §II.5 — the Aplicacao's typed
2785 /// external-entry composite; §V CSE invariants — "the external
2786 /// gateway is a first-class typed composite, not a per-Servico
2787 /// ingress annotation" the per-axis scalars enforce; §III.4 — the
2788 /// typed hostname + backend-Servico pair the per-cluster Gateway-
2789 /// API renderer keys off). Every per-`:entrada` axis threads
2790 /// through a lifted per-slot accessor on the
2791 /// [`crate::aplicacao::Entrada`] type: the
2792 /// [`crate::aplicacao::Entrada::host`] Gateway-API `Listener.hostname`
2793 /// scalar, the [`crate::aplicacao::Entrada::para`] backend-Servico
2794 /// caixa-name scalar, the [`crate::aplicacao::Entrada::paths`]
2795 /// per-rule `HTTPPathMatch` list, the [`crate::aplicacao::Entrada::port`]
2796 /// backend `trigger.service.port` scalar, and the
2797 /// [`crate::aplicacao::Entrada::resolved_paths`] URL-path fallback
2798 /// resolver every HTTPRoute-aware renderer consumes. Every
2799 /// downstream consumer that reaches for an entry axis first passes
2800 /// through this outer accessor onto the composite and then
2801 /// dispatches onto the per-axis accessor — the two-level dispatch
2802 /// means every per-`:entrada` reader now routes through a typed
2803 /// dispatch on the substrate primitive at both altitudes.
2804 ///
2805 /// Composes through [`Self::aplicacao_view`]'s Aplicacao-composition
2806 /// seed: the Aplicacao-view builder forwards the outer `Option`
2807 /// arm verbatim (no default fold — `:entrada` is inherently
2808 /// optional; a cluster-internal Aplicacao has no external gateway
2809 /// at all, not "an external gateway that defaults to nothing"), so
2810 /// the peer inner [`crate::AplicacaoSpec::entrada`] (d32111c)
2811 /// `Option<&Entrada>`-return accessor observes the same presence
2812 /// bit whether or not the author declared the outer slot. Routing
2813 /// the presence bit through this accessor keeps the
2814 /// [`Self::declared_mesh_slots`] M3 kind-coherence enumerator's
2815 /// `M3_AUTHOR_KEY_ENTRADA` push separate from the inner
2816 /// [`crate::AplicacaoSpec::validate_entrada`]-gated
2817 /// hostname/backend/path emission dispatch.
2818 ///
2819 /// Prior to this lift the `.entrada` `Option<Entrada>` composite
2820 /// was accessed inline at two production sites — the
2821 /// [`Self::aplicacao_view`] Aplicacao-composition seed's
2822 /// `self.entrada.clone()` traversal head (caixa-core/src/manifest.rs:2182,
2823 /// which drives the forward onto the peer inner
2824 /// [`crate::AplicacaoSpec::entrada`] accessor the caixa-mesh
2825 /// Gateway-API fan-out then observes), and the
2826 /// [`Self::declared_mesh_slots`] M3 declared-slot-set enumerator's
2827 /// `self.entrada.is_some()` presence probe (caixa-core/src/manifest.rs:2248,
2828 /// which drives the `M3_AUTHOR_KEY_ENTRADA` kebab-case author-
2829 /// label push every [`crate::LayoutError::MeshSlotsOnNonAplicacao`]
2830 /// kind-coherence gate reads) — two open-coded outer-field
2831 /// accesses that expressed no compile-time link back to the typed
2832 /// slot at the [`Caixa`] altitude. A future extension of the
2833 /// `:entrada` outer axis to a richer author surface (a per-cluster
2834 /// `:entrada-overrides` slot the operator materializes at admission
2835 /// time so a cluster-specific hostname can pin the caixa-declared
2836 /// bound without re-authoring the `caixa.lisp`, a per-tenant
2837 /// gateway-alias table the M4 `mesh.pleme.io/v1alpha1/Aplicacao`
2838 /// CR materializer resolves per-CR at admission time, a promotion
2839 /// of the plain `Option<Entrada>` to a richer
2840 /// `{public, private, internal}` partition once Cilium-identity-
2841 /// scoped internal gateways come into typed scope) would have had
2842 /// to be threaded through both open-coded copies in lockstep or the
2843 /// Aplicacao-composition seed's forward arm would silently
2844 /// disagree with the M3 declared-slot enumerator on which external-
2845 /// gateway composite a given Caixa resolves to — the seed reading
2846 /// an operator-resolved slot while the enumerator's presence probe
2847 /// read the raw slot would silently split the build-time gateway-
2848 /// artifact emission gate from the M3 declared-slot enumerator's
2849 /// kind-coherence gate, a two-consumer split far from the source
2850 /// `caixa.lisp` with no field naming the entry-drift root cause.
2851 /// Lifting the resolution rule to a typed method on the substrate
2852 /// primitive means every downstream consumer of the caixa's per-
2853 /// `Caixa` MESH-COMPOSITION external-gateway outer-composite
2854 /// surface reaches for exactly one typed dispatch — the resolver's
2855 /// accept-set migrates as a unit on any future axis addition.
2856 ///
2857 /// Fifth and final outer top-level [`Caixa`] `Option<&Composite>`-
2858 /// return composite-reference accessor — closes the outer-`Caixa`
2859 /// `Option<&Composite>` composite-reference sub-family opened by
2860 /// [`Self::limits`] (b2bd9d7) / [`Self::behavior`] (35d8b52) on the
2861 /// M2 Servico-runtime arm and extended onto the M3 mesh-slot arm
2862 /// by [`Self::politicas`] (5d23d29) / [`Self::placement`] (4fb8074),
2863 /// folds on the "one typed dispatch on the substrate primitive,
2864 /// thin projections at each consumer" discipline extended onto the
2865 /// third and final M3 mesh-slot axis. Peer of the closed inner
2866 /// mesh-slot outer-composite family the sibling
2867 /// [`crate::AplicacaoSpec::politicas`] (534dc21) /
2868 /// [`crate::AplicacaoSpec::placement`] (9abb8f0) /
2869 /// [`crate::AplicacaoSpec::entrada`] (d32111c) composite-reference
2870 /// accessor pins already close on the inner
2871 /// [`crate::AplicacaoSpec`] altitude — this lift closes the mirror
2872 /// sub-family on the outer top-level [`Caixa`] altitude, so both
2873 /// altitudes of the outer-composite reference-return discipline
2874 /// (per-`Caixa` outer-slot presence + per-`AplicacaoSpec` inner-
2875 /// slot presence) now carry the full five-arm accept-set behind a
2876 /// typed dispatch on the substrate primitive. Returns
2877 /// `Option<&Entrada>` (not the owning composite by copy or clone)
2878 /// because every downstream consumer of the entrada composite
2879 /// treats it as a read-only per-axis dispatch source — the
2880 /// reference-view is the narrowest borrow that supports every
2881 /// present + roadmapped consumer (per-axis accessor dispatch,
2882 /// serde composite-serialization on the programs.yaml overlay,
2883 /// presence-probe early return on the "author-omitted `:entrada`
2884 /// ⇒ cluster-internal Aplicacao" partition, `Aplicacao`-composition
2885 /// seed's forward arm) without cloning the composite through every
2886 /// consumer's fast path. The `Option` half of the return-type
2887 /// preserves the load-bearing "author-omitted `:entrada` ⇒
2888 /// cluster-internal Aplicacao" partition (not a default composite
2889 /// the downstream must reject on emptiness — a cluster-internal
2890 /// Aplicacao has no external gateway at all, not "a default gateway
2891 /// that emits nothing"); the accessor projects the raw
2892 /// `Option<Entrada>` slot's presence bit through the reference-
2893 /// return unchanged. Named `entrada()` to match the storage field's
2894 /// name verbatim and the tatara-lisp author-surface term
2895 /// (`:entrada`) the field's own docstring already carries.
2896 #[must_use]
2897 pub fn entrada(&self) -> Option<&crate::aplicacao::Entrada> {
2898 self.entrada.as_ref()
2899 }
2900
2901 /// Substrate-canonical per-`Caixa` `:ci` slot accessor — returns the
2902 /// author-declared typed CI run (`canteiro_types::CiRun`) verbatim as
2903 /// an `Option<&CiRun>`, borrowed from the typed slot's own
2904 /// `Option<CiRun>` storage. `None` when the slot is absent (every
2905 /// non-`Acao` kind, and an `Acao` caixa that hasn't declared `:ci`
2906 /// yet — the latter is caught by [`crate::LayoutError::MissingCi`],
2907 /// not silently accepted).
2908 ///
2909 /// Named `ci()` to match the storage field's name and the
2910 /// tatara-lisp author surface (`:ci`); mirrors the sibling
2911 /// `Option<&Composite>` accessors on this same `Caixa` altitude
2912 /// ([`Self::limits`], [`Self::behavior`], [`Self::politicas`],
2913 /// [`Self::placement`], [`Self::entrada`]) — one typed dispatch on
2914 /// the substrate primitive rather than an open-coded `self.ci.as_ref()`
2915 /// at every consumer.
2916 #[must_use]
2917 pub fn ci(&self) -> Option<&canteiro_types::CiRun> {
2918 self.ci.as_ref()
2919 }
2920
2921 /// Substrate-canonical per-`Caixa` `:estrategia` M2 supervisor-tree-
2922 /// slot flat-spread OTP-shaped sibling-restart-strategy discriminant
2923 /// accessor every consumer of the top-level manifest's per-Supervisor
2924 /// restart-strategy axis keys off — returns the author-declared
2925 /// `:estrategia` variant verbatim as an `Option<RestartStrategy>`,
2926 /// `Copy`-projected from the typed slot's own
2927 /// `Option<crate::supervisor::RestartStrategy>` storage. Optional
2928 /// (`:estrategia` is a flat-spread supervisor-only slot every
2929 /// non-`Supervisor`-kind `defcaixa` carries as `None` by
2930 /// `#[serde(default)]`, and every `Supervisor`-kind `defcaixa` may
2931 /// still omit to defer to [`RestartStrategy::default`] —
2932 /// [`RestartStrategy::OneForOne`] — through the [`Self::supervisor_view`]
2933 /// `unwrap_or_default()` fold; a returned `None` degenerates to the
2934 /// [`SupervisorSpec::default`]-inherited strategy without any silent
2935 /// promotion to a fresh explicit variant at the accessor boundary).
2936 ///
2937 /// The `:estrategia` slot carries the M2 typed OTP-shaped sibling-
2938 /// restart-strategy discriminant every substrate-side per-Supervisor
2939 /// dispatch fans on (INSPIRATIONS §II.2 — OTP `supervisor:strategy`
2940 /// closed-set `one_for_one | one_for_all | rest_for_one |
2941 /// simple_one_for_one` algebra translated onto pleme-io's typed
2942 /// [`RestartStrategy`] enum; CAIXA-SDLC §II — the M2 supervisor-tree
2943 /// slot algebra the operator's hierarchical reconciliation scheduler
2944 /// fans on). The slot is *flat-spread* on the outer top-level `Caixa`
2945 /// (per the field-shape docstring at caixa-core/src/manifest.rs — "The
2946 /// supervisor slots are flat on Caixa (vs nested under a
2947 /// `SupervisorSpec` sub-form) to keep tatara-lisp authoring at one
2948 /// level of nesting"), so the accessor's altitude is the outer
2949 /// [`Caixa`] surface rather than the composed [`SupervisorSpec`]
2950 /// altitude the sibling [`crate::supervisor::SupervisorSpec::estrategia`]
2951 /// (eafb619) accessor keys off. The two typed axes — the outer
2952 /// author-surface `Option<RestartStrategy>` on the [`Caixa`] altitude
2953 /// (author-omitted arm carried as `None`) and the inner post-
2954 /// composition `RestartStrategy` on the [`SupervisorSpec`] altitude
2955 /// (`Option` collapsed through the [`Self::supervisor_view`]
2956 /// `unwrap_or_default()` fold) — now share one accessor discipline for
2957 /// the shared substrate concept "the author-declared OTP-shaped
2958 /// sibling-restart-strategy variant that partitions the downstream
2959 /// per-Supervisor renderer's per-arm fan-out"; the outer-altitude
2960 /// `None` arm is the pre-composition presence bit every declared-slot
2961 /// enumerator ([`Self::declared_supervisor_slots`]) reads, and the
2962 /// inner-altitude non-`Option` `RestartStrategy` is the post-
2963 /// composition partition-dispatch input every strategy-arm consumer
2964 /// ([`SupervisorSpec::validate`], the future wasm-operator's per-
2965 /// Supervisor sibling-restart branch, the future M4
2966 /// `mesh.pleme.io/v1alpha1/Supervisor` CR materializer's admission
2967 /// webhook) fans on.
2968 ///
2969 /// Prior to this lift the `.estrategia` field was accessed inline at
2970 /// two production sites in `caixa-core/src/manifest.rs` — the
2971 /// [`Self::declared_supervisor_slots`] `SUPERVISOR_AUTHOR_KEY_ESTRATEGIA`
2972 /// presence-probe arm at `if self.estrategia.is_some()` (which drives
2973 /// the [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] kind-
2974 /// coherence gate's per-slot label push) and the [`Self::supervisor_view`]
2975 /// `SupervisorSpec` construction site at `estrategia:
2976 /// self.estrategia.unwrap_or_default()` (which composes the flat-
2977 /// spread outer author-surface `Option<RestartStrategy>` onto the
2978 /// inner post-composition [`SupervisorSpec`] `RestartStrategy` field
2979 /// the [`SupervisorSpec::estrategia`] accessor keys off) — two open-
2980 /// coded field-accesses that expressed no compile-time link back to
2981 /// the typed slot. A future extension of the outer `:estrategia` axis
2982 /// to a richer author surface (a per-cluster strategy override the
2983 /// operator pins through a future `:estrategia-overrides` overlay the
2984 /// MESH-COMPOSITION §III.2 supervision-canary roadmap acknowledges,
2985 /// a per-tenant strategy-alias table the M4 CR materializer resolves
2986 /// per-CR, a per-Supervisor dynamic strategy derivation the future
2987 /// adaptive-supervision engine computes from child-failure-history
2988 /// topology, a per-child-cohort strategy split the future
2989 /// `RestForCohort` extension the INSPIRATIONS.md §II.2 Erlang/OTP
2990 /// absorption roadmap acknowledges, a promotion of the plain
2991 /// `Option<RestartStrategy>` to a richer
2992 /// `AuthorDeclaredStrategy { declared, overlay }` newtype once the
2993 /// operator-resolved overlay lands) would have had to be threaded
2994 /// through both open-coded copies in lockstep or the enumerator's
2995 /// presence probe and the composition site's `unwrap_or_default()`
2996 /// fold would silently disagree on which strategy a given [`Caixa`]
2997 /// resolves to (an author's `:estrategia OneForAll` would satisfy
2998 /// the enumerator's presence probe while the composition site
2999 /// silently rendered a stale `OneForOne`, or vice versa). Lifting
3000 /// the resolution rule to a typed method on the substrate primitive
3001 /// means every downstream consumer of the caixa's per-`Caixa` outer-
3002 /// altitude sibling-restart-strategy surface reaches for exactly one
3003 /// typed dispatch — the resolver's accept-set migrates as a unit on
3004 /// any future axis addition.
3005 ///
3006 /// First outer top-level [`Caixa`] `Option<Copy>`-return supervisor-
3007 /// tree-slot flat-spread accessor for M2 supervisor-slot Copy-carry
3008 /// axes — opens the outer-`Caixa` `Option<Copy>` flat-spread
3009 /// projection pattern the sibling per-`Caixa` `:max-restarts`
3010 /// `Option<u32>` and (through the future duration-newtype landing)
3011 /// `:restart-window` `Option<Duration>` future outer-scalar lifts
3012 /// fold on. Peer of the inner-altitude [`crate::supervisor::SupervisorSpec::estrategia`]
3013 /// (eafb619) `Copy`-return sibling-restart-strategy scalar accessor on
3014 /// the post-composition [`SupervisorSpec`] altitude — same "one
3015 /// typed dispatch on the substrate primitive, thin projections at
3016 /// each consumer" discipline extended onto the pre-composition outer
3017 /// author-surface [`Caixa`] altitude for the same OTP-shaped
3018 /// sibling-restart-strategy axis. Peer of the closed outer-`Caixa`
3019 /// `Option<&Composite>` composite-reference family the sibling
3020 /// [`Self::limits`] (b2bd9d7) / [`Self::behavior`] (35d8b52) /
3021 /// [`Self::politicas`] (5d23d29) / [`Self::placement`] (4fb8074) /
3022 /// [`Self::entrada`] (e4128e4) accessor pins already carry on the
3023 /// outer `Option<&Composite>` altitude — extends the outer-`Caixa`
3024 /// typed-slot accessor discipline onto the flat-spread M2 supervisor-
3025 /// tree `Option<Copy>`-discriminant sub-family the sibling M3
3026 /// [`crate::aplicacao::Placement::estrategia`] (921fe1b)
3027 /// `PlacementStrategy` `Copy`-composite-enum scalar accessor already
3028 /// pins on the inner-altitude per-`:placement` composite. Named
3029 /// `estrategia()` to match the storage field's name and the
3030 /// per-[`SupervisorSpec`] peer [`crate::supervisor::SupervisorSpec::estrategia`]
3031 /// / per-[`crate::aplicacao::Placement`] peer
3032 /// [`crate::aplicacao::Placement::estrategia`] method-name discipline
3033 /// verbatim; the accessor's identity name maps onto the canonical
3034 /// OTP-shape supervision vocabulary the [`RestartStrategy`] enum's
3035 /// docstring already carries.
3036 #[must_use]
3037 pub fn estrategia(&self) -> Option<crate::supervisor::RestartStrategy> {
3038 self.estrategia
3039 }
3040
3041 /// Substrate-canonical per-`Caixa` `:max-restarts` M2 supervisor-tree-
3042 /// slot flat-spread OTP-`MaxIntensity`-shaped restart-budget-count
3043 /// scalar accessor every consumer of the top-level manifest's per-
3044 /// Supervisor `:max-restarts` restart-budget-count axis keys off —
3045 /// returns the author-declared `:max-restarts` typed `Option<u32>`
3046 /// verbatim, `Copy`-projected from the typed slot's own `Option<u32>`
3047 /// storage (`u32` is `Copy`, so `Option<u32>` is `Copy` and the
3048 /// accessor returns by value; no borrow of `&self` past the call).
3049 /// Optional (`:max-restarts` is a flat-spread supervisor-only slot
3050 /// every non-`Supervisor`-kind `defcaixa` carries as `None` by
3051 /// `#[serde(default)]`, and every `Supervisor`-kind `defcaixa` may
3052 /// still omit to defer to the [`Self::supervisor_view`]
3053 /// `unwrap_or(5)` fold's OTP-canonical `{intensity, 5, 60}` default).
3054 ///
3055 /// The `:max-restarts` slot carries the M2 typed Erlang/OTP-shaped
3056 /// `MaxIntensity` restart-budget count that pairs with the sibling
3057 /// `:restart-window` `Period` to form the `MaxIntensity / Period`
3058 /// restart-intensity ratio the supervisor trips its own escalation on
3059 /// (INSPIRATIONS §II.2 — Erlang/OTP `supervisor` `{intensity, 5, 60}`
3060 /// worker-supervisor default; RUNTIME-PATTERNS §II.2; CAIXA-SDLC §II
3061 /// — the M2 supervisor-tree slot algebra the operator's hierarchical
3062 /// reconciliation scheduler fans on). The slot is *flat-spread* on
3063 /// the outer top-level `Caixa` (per the field-shape docstring at
3064 /// caixa-core/src/manifest.rs — "The supervisor slots are flat on
3065 /// Caixa (vs nested under a `SupervisorSpec` sub-form)"), so the
3066 /// accessor's altitude is the outer [`Caixa`] surface rather than the
3067 /// composed [`SupervisorSpec`] altitude the sibling
3068 /// [`crate::supervisor::SupervisorSpec::max_restarts`] accessor keys
3069 /// off. The two typed axes — the outer author-surface `Option<u32>`
3070 /// on the [`Caixa`] altitude (author-omitted arm carried as `None`)
3071 /// and the inner post-composition `u32` on the [`SupervisorSpec`]
3072 /// altitude (`Option` collapsed through the [`Self::supervisor_view`]
3073 /// `unwrap_or(5)` fold) — now share one accessor discipline for the
3074 /// shared substrate concept "the author-declared OTP-shaped
3075 /// restart-budget count every downstream per-Supervisor consumer's
3076 /// restart-intensity budget-vs-count comparator fans on".
3077 ///
3078 /// Prior to this lift the `.max_restarts` field was accessed inline
3079 /// at two production sites in `caixa-core/src/manifest.rs` — the
3080 /// [`Self::declared_supervisor_slots`] `SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS`
3081 /// presence-probe arm at `if self.max_restarts.is_some()` (which
3082 /// drives the [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
3083 /// kind-coherence gate's per-slot label push) and the
3084 /// [`Self::supervisor_view`] `SupervisorSpec` construction site at
3085 /// `max_restarts: self.max_restarts.unwrap_or(5)` (which composes the
3086 /// flat-spread outer author-surface `Option<u32>` onto the inner
3087 /// post-composition [`SupervisorSpec`] `u32` field the
3088 /// [`SupervisorSpec::max_restarts`] accessor keys off) — two open-
3089 /// coded field-accesses that expressed no compile-time link back to
3090 /// the typed slot. A future extension of the outer `:max-restarts`
3091 /// axis to a richer author surface (a per-cluster restart-budget
3092 /// override the operator pins through a future `:max-restarts-overrides`
3093 /// overlay the MESH-COMPOSITION §III.2 supervision-canary roadmap
3094 /// acknowledges, a per-tenant restart-budget-alias table the M4 CR
3095 /// materializer resolves per-CR, a per-Supervisor dynamic restart-
3096 /// budget derivation the future adaptive-supervision engine computes
3097 /// from child-failure-history topology, a promotion of the plain
3098 /// `Option<u32>` count to a richer `{MaxR, MaxT}` per-child-cohort
3099 /// restart-budget-partition once the INSPIRATIONS §II.2 Erlang/OTP
3100 /// per-child-cohort roadmap lands) would have had to be threaded
3101 /// through both open-coded copies in lockstep or the enumerator's
3102 /// presence probe and the composition site's `unwrap_or(5)` fold
3103 /// would silently disagree on which restart-budget a given [`Caixa`]
3104 /// resolves to (an author's `:max-restarts 10` would satisfy the
3105 /// enumerator's presence probe while the composition site silently
3106 /// composed the OTP-canonical `5`, or vice versa). Lifting the
3107 /// resolution rule to a typed method on the substrate primitive means
3108 /// every downstream consumer of the caixa's per-`Caixa` outer-altitude
3109 /// restart-budget-count surface reaches for exactly one typed dispatch
3110 /// — the resolver's accept-set migrates as a unit on any future axis
3111 /// addition.
3112 ///
3113 /// Second outer top-level [`Caixa`] `Option<Copy>`-return supervisor-
3114 /// tree-slot flat-spread accessor for M2 supervisor-slot Copy-carry
3115 /// axes — folds on the outer-`Caixa` `Option<Copy>` flat-spread
3116 /// projection pattern the sibling per-`Caixa`
3117 /// [`Self::estrategia`] (ed04d3c) accessor opened, extends the
3118 /// sub-family onto the sibling `Option<u32>` restart-budget-count arm.
3119 /// Peer of the inner-altitude
3120 /// [`crate::supervisor::SupervisorSpec::max_restarts`] `u32` accessor
3121 /// on the post-composition [`SupervisorSpec`] altitude — same "one
3122 /// typed dispatch on the substrate primitive, thin projections at
3123 /// each consumer" discipline extended onto the pre-composition outer
3124 /// author-surface [`Caixa`] altitude for the same OTP-`MaxIntensity`-
3125 /// shaped restart-budget-count axis. Named `max_restarts()` to match
3126 /// the storage field's name and the per-[`SupervisorSpec`] peer
3127 /// [`crate::supervisor::SupervisorSpec::max_restarts`] method-name
3128 /// discipline verbatim; the accessor's identity maps onto the
3129 /// canonical OTP-shape supervision vocabulary the `:max-restarts`
3130 /// field's docstring already carries.
3131 #[must_use]
3132 pub const fn max_restarts(&self) -> Option<u32> {
3133 self.max_restarts
3134 }
3135
3136 /// Substrate-canonical per-`Caixa` `:restart-window` M2 supervisor-
3137 /// tree-slot flat-spread OTP-`Period`-shaped restart-intensity-
3138 /// denominator raw-duration-string scalar accessor every consumer of
3139 /// the top-level manifest's per-Supervisor `:restart-window` sliding-
3140 /// window axis keys off — returns the author-declared `:restart-window`
3141 /// typed `Option<String>` verbatim as an `Option<&str>`, borrowed
3142 /// from the typed slot's own `Option<String>` storage. `None` when
3143 /// the slot is absent (the canonical "never reset — every restart
3144 /// across the supervisor's lifetime counts against the sibling
3145 /// `:max-restarts` budget" sentinel every non-`Supervisor`-kind
3146 /// `defcaixa` carries by `#[serde(default)]` and every
3147 /// `Supervisor`-kind `defcaixa` may still omit to defer to the
3148 /// [`Self::supervisor_view`] `restart_window: None` composition
3149 /// through the [`crate::supervisor::duration_codec::parse`] soft-
3150 /// swallow `.and_then(|s| … .ok())` fold).
3151 ///
3152 /// The `:restart-window` slot carries the raw M2 typed Erlang/OTP-
3153 /// shaped `Period` sliding-observation-interval duration string that
3154 /// pairs with the sibling `:max-restarts` `MaxIntensity` restart-
3155 /// budget count to form the `MaxIntensity / Period` restart-intensity
3156 /// ratio the supervisor trips its own escalation on (INSPIRATIONS
3157 /// §II.2 — Erlang/OTP `supervisor` `{intensity, 5, 60}` worker-
3158 /// supervisor default; RUNTIME-PATTERNS §II.2). The outer-`Caixa`
3159 /// slot stores the raw duration string (`"60s"`, `"5m"`, `"500ms"`)
3160 /// authored under `:restart-window` — the typed [`SupervisorSpec`]
3161 /// holds an `Option<Duration>` routed through the shared
3162 /// [`crate::supervisor::duration_codec`] via `with = "duration_codec"`
3163 /// — so the outer altitude's accessor returns `Option<&str>` (raw
3164 /// authoring surface) while the inner altitude's
3165 /// [`crate::supervisor::SupervisorSpec::restart_window`] returns
3166 /// `Option<Duration>` (parsed typed surface). The parse-refusal arm
3167 /// is closed by the sibling [`Self::validate_restart_window`] gate
3168 /// that surfaces [`ManifestError::RestartWindowMalformed`] naming
3169 /// the offending value; the view-construction path
3170 /// [`Self::supervisor_view`] soft-swallows the same parse error to
3171 /// `None` to keep the view best-effort.
3172 ///
3173 /// Prior to this lift the `.restart_window` field was accessed inline
3174 /// at three production sites in `caixa-core/src/manifest.rs` — the
3175 /// [`Self::declared_supervisor_slots`]
3176 /// `SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW` presence-probe arm at
3177 /// `if self.restart_window.is_some()` (which drives the
3178 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] kind-
3179 /// coherence gate's per-slot label push), the
3180 /// [`Self::validate_restart_window`] `let Some(s) =
3181 /// self.restart_window.as_deref()` empty-and-shape gate binding
3182 /// (which folds the raw string through the shared
3183 /// [`crate::supervisor::duration_codec::parse`] to surface
3184 /// [`ManifestError::RestartWindowMalformed`] naming the offending
3185 /// value), and the [`Self::supervisor_view`] `self.restart_window
3186 /// .as_deref().and_then(…)` view-construction fold (which composes
3187 /// the flat-spread outer author-surface `Option<String>` onto the
3188 /// inner post-composition [`SupervisorSpec`] `Option<Duration>`
3189 /// field the [`SupervisorSpec::restart_window`] accessor keys off) —
3190 /// three open-coded field-accesses that expressed no compile-time
3191 /// link back to the typed slot. A future extension of the outer
3192 /// `:restart-window` axis to a richer author surface (a per-cluster
3193 /// window override, a per-tenant window-alias table, a per-Supervisor
3194 /// dynamic window derivation the future adaptive-supervision engine
3195 /// computes from child-failure-history topology, a promotion of the
3196 /// plain `Option<String>` raw duration to a typed `Option<Duration>`
3197 /// once the future author-surface parser lands at the [`Caixa`]
3198 /// altitude and the raw-string form is retired) would have had to be
3199 /// threaded through every open-coded copy in lockstep or the three
3200 /// consumers would silently disagree on which raw string a given
3201 /// [`Caixa`] resolves to. Lifting the resolution rule to a typed
3202 /// method on the substrate primitive means every downstream consumer
3203 /// of the caixa's per-`Caixa` outer-altitude restart-window raw-
3204 /// string surface reaches for exactly one typed dispatch — the
3205 /// resolver's accept-set migrates as a unit on any future axis
3206 /// addition.
3207 ///
3208 /// Third outer top-level [`Caixa`] supervisor-tree-slot flat-spread
3209 /// accessor — folds on the outer-`Caixa` M2 supervisor-tree flat-
3210 /// spread projection pattern the sibling per-`Caixa`
3211 /// [`Self::estrategia`] (ed04d3c) `Option<Copy>` and
3212 /// [`Self::max_restarts`] `Option<Copy>` accessors opened, extends
3213 /// the sub-family onto the sibling `Option<&str>` raw-duration-
3214 /// string arm (the outer altitude's raw-string form; the inner
3215 /// altitude's parsed [`Duration`] form is the peer
3216 /// [`crate::supervisor::SupervisorSpec::restart_window`] accessor).
3217 /// Peer of the sibling per-`Caixa` `Option<&str>`-return scalar
3218 /// accessors ([`Self::licenca`] / [`Self::repositorio`] /
3219 /// [`Self::descricao`] / [`Self::edicao`]) on the universal-axis
3220 /// outer scalar-projection family the outer-`Caixa` `Option<&str>`
3221 /// sub-family already carries — same "one typed dispatch on the
3222 /// substrate primitive, thin projections at each consumer"
3223 /// discipline extended onto the M2 supervisor-tree flat-spread
3224 /// `Option<&str>` raw-duration-string arm. Named `restart_window()`
3225 /// to match the storage field's name and the per-[`SupervisorSpec`]
3226 /// peer [`crate::supervisor::SupervisorSpec::restart_window`]
3227 /// method-name discipline verbatim; the accessor's identity maps
3228 /// onto the canonical OTP-shape supervision vocabulary the
3229 /// `:restart-window` field's docstring already carries.
3230 #[must_use]
3231 pub fn restart_window(&self) -> Option<&str> {
3232 self.restart_window.as_deref()
3233 }
3234
3235 /// Substrate-canonical per-`Caixa` `:upgrade-from` M2 typed-slot
3236 /// outer-composite OTP-appup-shaped per-prior-version migration-
3237 /// entry-list slice accessor every consumer of the top-level
3238 /// manifest's per-Servico hot-upgrade-block `&[UpgradeFromEntry]`
3239 /// slice-view keys off — returns the author-declared `:upgrade-from`
3240 /// typed `Vec<UpgradeFromEntry>` verbatim as a
3241 /// `&[UpgradeFromEntry]` slice-view over the same backing buffer
3242 /// the raw `self.upgrade_from.as_slice()` field access borrows
3243 /// from. Empty-slice-carrying (the "no hot-upgrade path declared"
3244 /// arm every `defcaixa` without an `:upgrade-from` block carries;
3245 /// the [`Self::from_lisp`] derive folds an omitted `:upgrade-from`
3246 /// through `#[serde(default)]` to `Vec::new()`, so a `Caixa` past
3247 /// parse definitionally carries a `Vec<UpgradeFromEntry>` slot —
3248 /// possibly empty — and the returned `&[UpgradeFromEntry]`
3249 /// degenerates to an empty slice on that arm without any silent
3250 /// `None` collapse).
3251 ///
3252 /// The outer `:upgrade-from` slot carries the M2 typed OTP-appup
3253 /// migration block — the load-bearing container of every per-
3254 /// prior-`:versao` migration-instruction list the wasm-operator
3255 /// dispatches on at hot-upgrade time (INSPIRATIONS §II.4 — OTP
3256 /// `.appup` per-prior-version `LoadModule | StateChange |
3257 /// SoftPurge | Purge | Restart` instruction algebra translated
3258 /// onto pleme-io's typed `:upgrade-from :from` + `:instructions`
3259 /// entry list; CAIXA-SDLC §II — the typed-M2 slot algebra the
3260 /// operator's hot-upgrade dispatch fans on). Every per-entry axis
3261 /// threads through a lifted per-entry accessor on the
3262 /// [`UpgradeFromEntry`] type: the
3263 /// [`UpgradeFromEntry::prior_versao`] SemVer-shaped previous-
3264 /// version scalar accessor and the
3265 /// [`UpgradeFromEntry::instructions`] `&[UpgradeInstruction]`-
3266 /// return per-entry instruction-list accessor (0137e5a). Every
3267 /// downstream consumer of the hot-upgrade path first passes
3268 /// through this outer accessor onto the slice and then dispatches
3269 /// per-entry through the inner accessors — the two-level dispatch
3270 /// means every per-`:upgrade-from` reader now routes through a
3271 /// typed dispatch on the substrate primitive at both altitudes.
3272 ///
3273 /// Prior to this lift the `.upgrade_from` `Vec<UpgradeFromEntry>`
3274 /// slot was accessed inline at production sites across three
3275 /// files — the [`Self::declared_servico_slots`] M2 declared-slot
3276 /// enumerator's `self.upgrade_from.is_empty()` presence probe
3277 /// (caixa-core/src/manifest.rs, which drives the
3278 /// `M2_AUTHOR_KEY_UPGRADE_FROM` kebab-case author-label push every
3279 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] kind-coherence
3280 /// gate reads), the [`crate::StandardLayout::verify`] per-
3281 /// `:upgrade-from` three-stage validation pass (caixa-core/src/
3282 /// layout.rs, which fans onto the
3283 /// [`crate::upgrade::validate_upgrade_from`] per-entry shape +
3284 /// cross-entry duplicate gate, the
3285 /// [`crate::upgrade::validate_upgrade_from_against_versao`]
3286 /// SemVer-precedence cross-slot gate, the
3287 /// [`crate::upgrade::validate_upgrade_from_against_behavior`]
3288 /// `:state-change` ↔ `:on-state-change` cross-slot composition
3289 /// gate, and the per-instruction script-path existence-probe walk
3290 /// that reads each entry's [`UpgradeFromEntry::instructions`] to
3291 /// resolve every declared migration script against the layout
3292 /// root), and the [`crate::render::servico_m2_overlay`] per-
3293 /// Servico M2 overlay emitter's `!caixa.upgrade_from.is_empty()`
3294 /// presence gate + `serde_yaml::to_value(&caixa.upgrade_from)`
3295 /// projection (caixa-core/src/render.rs, which drives the
3296 /// `M2_KEY_UPGRADE_FROM`-keyed `serde_yaml` projection every
3297 /// `caixa-helm` / `caixa-flux` Servico values-block emitter fans
3298 /// on and lands as the ComputeUnit CR's `spec.upgradeFrom` field).
3299 /// A future extension of the outer `:upgrade-from` axis (a per-
3300 /// cluster `:upgrade-overrides` overlay the wasm-engine operator
3301 /// resolves at admission time so a cluster-specific migration
3302 /// policy can tighten a caixa-declared step without re-authoring
3303 /// the `caixa.lisp`, promotion of the plain
3304 /// `Vec<UpgradeFromEntry>` to a richer `{static, dynamic}`
3305 /// partition once runtime-resolved hot-upgrade instructions land,
3306 /// per-entry priority annotation once multi-strategy fan-out
3307 /// lands) would have had to be threaded through all six open-
3308 /// coded copies in lockstep or one consumer would silently
3309 /// disagree with the peers on which upgrade slice a given Caixa
3310 /// resolves to — a six-consumer split at the enumerator, the
3311 /// three-stage validate pass, the script-path probe walk, and the
3312 /// M2 overlay emitter, far from the source `caixa.lisp` with no
3313 /// field naming the upgrade-drift root cause. Lifting the
3314 /// resolution rule to a typed method on the substrate primitive
3315 /// means every downstream consumer of the caixa's per-`Caixa`
3316 /// OTP-appup outer-slice surface reaches for exactly one typed
3317 /// dispatch — the resolver's accept-set migrates as a unit on any
3318 /// future axis addition.
3319 ///
3320 /// First outer top-level [`Caixa`] `&[Composite]`-return slice
3321 /// accessor for M2 / M3 typed-slot vec-carry axes — opens the
3322 /// outer-`Caixa` `&[Composite]` composite-slice projection
3323 /// pattern the sibling `:children`
3324 /// [`crate::supervisor::ChildSpec`] / `:membros`
3325 /// [`crate::aplicacao::Membro`] / `:contratos`
3326 /// [`crate::aplicacao::WitContract`] future outer-composite-slice
3327 /// lifts fold on. Peer of the closed outer-`Caixa` scalar
3328 /// `Option<&Composite>` composite-reference family the sibling
3329 /// [`Self::limits`] (b2bd9d7) / [`Self::behavior`] (35d8b52) /
3330 /// [`Self::politicas`] (5d23d29) / [`Self::placement`] (4fb8074) /
3331 /// [`Self::entrada`] (e4128e4) accessors closed on the outer
3332 /// `Option<&Composite>` altitude, extended here to the outer-
3333 /// `Caixa` `&[Composite]` vec-carry altitude. Peer at the inner
3334 /// altitude of [`crate::upgrade::UpgradeFromEntry::instructions`]
3335 /// (0137e5a) — same "one typed dispatch on the substrate
3336 /// primitive, thin projections at each consumer" discipline
3337 /// folded onto the outer top-level [`Caixa`] altitude, opening the
3338 /// M2 vec-carry slot family's outer-composite-slice axis. Sibling
3339 /// in shape to the peer outer-`Caixa` `&[Dep]`-return
3340 /// [`Self::deps`] (ad34b4e) / [`Self::deps_dev`] (f7fd81e) and
3341 /// `&[String]`-return [`Self::autores`] (b5d813f) /
3342 /// [`Self::etiquetas`] (78c7d3c) / [`Self::bibliotecas`]
3343 /// (8a36c23) / [`Self::exe`] (65d9527) / [`Self::servicos`]
3344 /// (611f78b) slice-accessors on the sibling outer-`Caixa` scalar-
3345 /// element vec-carry axes — folds the "outer [`Caixa`] `&[T]`
3346 /// slice" projection pattern onto the sibling M2 typed-composite-
3347 /// element axis (`UpgradeFromEntry` composite, matching the
3348 /// per-inner [`UpgradeFromEntry::instructions`] element type at a
3349 /// different altitude).
3350 ///
3351 /// Returns `&[UpgradeFromEntry]` (not `&Vec<UpgradeFromEntry>`)
3352 /// because every downstream consumer of the hot-upgrade list
3353 /// treats it as a read-only sequence — the slice-view is the
3354 /// narrowest borrow that supports every present + roadmapped
3355 /// consumer (`.iter()`, `.len()`, `.is_empty()`, `serde` slice-
3356 /// serialization through
3357 /// `serde_yaml::to_value(&[UpgradeFromEntry])`) without leaking
3358 /// the backing `Vec`'s grow/push/reserve surface no consumer of
3359 /// the typed view reaches for (the storage-side `Vec` remains
3360 /// reachable through the `pub upgrade_from` field for the
3361 /// mutation-carrying serde round-trip and per-test fixture-
3362 /// mutation paths). Named `upgrade_from()` to match the storage
3363 /// field's `snake_case` name; the kebab-case author-surface tag
3364 /// `:upgrade-from` is the same axis after tatara-lisp's
3365 /// kebab↔snake fold and the accessor's identity maps onto the
3366 /// canonical CAIXA-SDLC §II vocabulary the slot's docstring
3367 /// already carries.
3368 #[must_use]
3369 pub fn upgrade_from(&self) -> &[UpgradeFromEntry] {
3370 self.upgrade_from.as_slice()
3371 }
3372
3373 /// Substrate-canonical per-`Caixa` `:children` M2 supervisor-tree-
3374 /// slot outer-composite OTP-shaped per-supervisor static-child-list
3375 /// slice accessor every consumer of the top-level manifest's per-
3376 /// Supervisor `&[ChildSpec]` slice-view keys off — returns the
3377 /// author-declared `:children` typed `Vec<crate::supervisor::ChildSpec>`
3378 /// verbatim as a `&[crate::supervisor::ChildSpec]` slice-view over
3379 /// the same backing buffer the raw `self.children.as_slice()` field
3380 /// access borrows from. Empty-slice-carrying (the "no static children
3381 /// declared" arm every non-`Supervisor`-kind `defcaixa` carries by
3382 /// #[serde(default)] and every `SimpleOneForOne` supervisor carries
3383 /// by [`crate::supervisor::SupervisorError::SimpleOneForOneWithStaticChildren`]
3384 /// gate; the returned `&[ChildSpec]` degenerates to an empty slice
3385 /// on those arms without any silent `None` collapse).
3386 ///
3387 /// The outer `:children` slot carries the M2 typed OTP-supervisor
3388 /// static-child list — the load-bearing container of every per-
3389 /// child `{caixa, versao, restart}` triple the wasm-operator's
3390 /// hierarchical reconciler dispatches on at supervisor-tree
3391 /// materialization time (INSPIRATIONS §II.2 — OTP `supervisor:init/1`
3392 /// static-child list translated onto pleme-io's typed
3393 /// [`crate::supervisor::ChildSpec`] entry list; CAIXA-SDLC §II —
3394 /// the typed-M2 slot algebra the operator's per-supervisor fan-out
3395 /// dispatch fans on). Every per-child axis threads through a lifted
3396 /// per-entry accessor on the [`crate::supervisor::ChildSpec`] type:
3397 /// the [`crate::supervisor::ChildSpec::nome`] DNS-1123-label
3398 /// child-caixa-identity scalar accessor, the peer versao SemVer-2
3399 /// version-requirement scalar accessor, and the
3400 /// [`crate::supervisor::ChildSpec::restart`] `Copy`-composite-enum
3401 /// per-child post-exit restart-decision-policy discriminant
3402 /// accessor (dfb4a81). Every downstream consumer of the supervisor-
3403 /// tree path first passes through this outer accessor onto the
3404 /// slice and then dispatches per-child through the inner accessors
3405 /// — the two-level dispatch means every per-`:children` reader now
3406 /// routes through a typed dispatch on the substrate primitive at
3407 /// both altitudes.
3408 ///
3409 /// Prior to this lift the `.children` `Vec<ChildSpec>` slot was
3410 /// accessed inline at three production sites across two files —
3411 /// the [`Self::declared_supervisor_slots`] supervisor-tree
3412 /// declared-slot enumerator's `!self.children.is_empty()` presence
3413 /// probe (caixa-core/src/manifest.rs, which drives the
3414 /// `SUPERVISOR_AUTHOR_KEY_CHILDREN` kebab-case author-label push
3415 /// every [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
3416 /// kind-coherence gate reads), the [`Self::supervisor_view`]
3417 /// per-supervisor typed-view composer's `self.children.clone()`
3418 /// per-child fold-in path (caixa-core/src/manifest.rs, which
3419 /// materializes the typed [`crate::supervisor::SupervisorSpec`]
3420 /// view every [`crate::StandardLayout::verify`] Supervisor-arm gate
3421 /// dispatches on), and the [`crate::StandardLayout::verify`] per-
3422 /// `:children :caixa` self-parent refusal probe's
3423 /// `&caixa.children`-borrowed
3424 /// [`crate::supervisor::validate_no_self_supervision`] input
3425 /// (caixa-core/src/layout.rs, which pins the "no child names the
3426 /// supervisor's own `:nome`" cross-slot coherence gate). A future
3427 /// extension of the outer `:children` axis (a per-cluster
3428 /// `:children-overrides` overlay the wasm-engine operator resolves
3429 /// at admission time so a cluster-specific child-set can tighten
3430 /// a caixa-declared list without re-authoring the `caixa.lisp`,
3431 /// promotion of the plain `Vec<ChildSpec>` to a richer
3432 /// `{static, dynamic}` partition once Erlang/OTP's
3433 /// `simple_one_for_one`-shaped dynamic-child slot lands as a typed
3434 /// axis, per-child priority annotation once multi-strategy fan-out
3435 /// lands) would have had to be threaded through all three open-
3436 /// coded copies in lockstep or one consumer would silently
3437 /// disagree with the peers on which child slice a given Caixa
3438 /// resolves to — the enumerator's presence probe reading the raw
3439 /// slot while the peer view-composer's fold-in path read an
3440 /// operator-resolved slot would silently split the paired
3441 /// declared-slot enumerator and typed-view composition, and the
3442 /// [`crate::supervisor::validate_no_self_supervision`] self-parent
3443 /// refusal probe reading a third borrow would silently drift the
3444 /// cross-slot coherence gate's traversal input from the two peers,
3445 /// a three-consumer split at the enumerator, the view composer,
3446 /// and the self-parent gate far from the source `caixa.lisp` with
3447 /// no field naming the child-set-drift root cause. Lifting the
3448 /// resolution rule to a typed method on the substrate primitive
3449 /// means every downstream consumer of the caixa's per-`Caixa`
3450 /// OTP-supervisor outer-slice surface reaches for exactly one
3451 /// typed dispatch — the resolver's accept-set migrates as a unit
3452 /// on any future axis addition.
3453 ///
3454 /// Second outer top-level [`Caixa`] `&[Composite]`-return slice
3455 /// accessor for M2 / M3 typed-slot vec-carry axes — folds on the
3456 /// outer-`Caixa` `&[Composite]` composite-slice sub-family the
3457 /// sibling [`Self::upgrade_from`] (2a1f907) accessor opened, peer
3458 /// at the outer altitude of the closed inner-`SupervisorSpec`
3459 /// [`crate::SupervisorSpec::children`] (bc92bce) accessor on the
3460 /// same OTP-supervisor static-child-list axis — same "byte-equal,
3461 /// borrow-shared" outer-accessor discipline extended onto the
3462 /// second outer-`Caixa` `&[Composite]` vec-carry axis. Sibling in
3463 /// shape to the peer outer-`Caixa` `&[Dep]`-return [`Self::deps`]
3464 /// (ad34b4e) / [`Self::deps_dev`] (f7fd81e) and `&[String]`-return
3465 /// [`Self::autores`] (b5d813f) / [`Self::etiquetas`] (78c7d3c) /
3466 /// [`Self::bibliotecas`] (8a36c23) / [`Self::exe`] (65d9527) /
3467 /// [`Self::servicos`] (611f78b) slice-accessors on the sibling
3468 /// outer-`Caixa` scalar-element vec-carry axes — folds the "outer
3469 /// [`Caixa`] `&[T]` slice" projection pattern onto the sibling
3470 /// M2 typed-composite-element axis
3471 /// ([`crate::supervisor::ChildSpec`] composite, matching the
3472 /// per-inner [`crate::SupervisorSpec::children`] element type at a
3473 /// different altitude).
3474 ///
3475 /// Returns `&[crate::supervisor::ChildSpec]` (not
3476 /// `&Vec<ChildSpec>`) because every downstream consumer of the
3477 /// child list treats it as a read-only sequence — the slice-view
3478 /// is the narrowest borrow that supports every present +
3479 /// roadmapped consumer (`.iter()`, `.len()`, `.is_empty()`, the
3480 /// [`crate::supervisor::validate_no_self_supervision`] `&[ChildSpec]`
3481 /// input, `serde` slice-serialization) without leaking the backing
3482 /// `Vec`'s grow/push/reserve surface no consumer of the typed view
3483 /// reaches for (the storage-side `Vec` remains reachable through
3484 /// the `pub children` field for the mutation-carrying serde round-
3485 /// trip and per-test fixture-mutation paths, including the
3486 /// [`Self::supervisor_view`] fold-in path that clones the slot
3487 /// into the typed view). Named `children()` to match the storage
3488 /// field's name verbatim and the tatara-lisp author-surface term
3489 /// (`:children`) the field's own docstring already carries; the
3490 /// accessor's identity maps onto the canonical OTP supervision
3491 /// vocabulary the [`Caixa::children`] field's docstring already
3492 /// reaches for ("Static children of a supervisor").
3493 #[must_use]
3494 pub fn children(&self) -> &[crate::supervisor::ChildSpec] {
3495 self.children.as_slice()
3496 }
3497
3498 /// Substrate-canonical per-`Caixa` `:membros` M3 mesh-slot outer-
3499 /// composite MESH-COMPOSITION-shaped per-Aplicacao member-list slice
3500 /// accessor every consumer of the top-level manifest's per-Aplicacao
3501 /// `&[crate::aplicacao::Membro]` slice-view keys off — returns the
3502 /// author-declared `:membros` typed `Vec<crate::aplicacao::Membro>`
3503 /// verbatim as a `&[crate::aplicacao::Membro]` slice-view over the
3504 /// same backing buffer the raw `self.membros.as_slice()` field access
3505 /// borrows from. Empty-slice-carrying (the "no members declared" arm
3506 /// every non-`Aplicacao`-kind `defcaixa` carries by `#[serde(default)]`
3507 /// and every partially-authored Aplicacao carries before the
3508 /// [`crate::AplicacaoError::MembrosEmpty`] gate fires; the returned
3509 /// `&[Membro]` degenerates to an empty slice on those arms without any
3510 /// silent `None` collapse).
3511 ///
3512 /// The outer `:membros` slot carries the M3 typed MESH-COMPOSITION
3513 /// per-Aplicacao member list — the load-bearing container of every
3514 /// per-member `{caixa, versao}` pair the caixa-mesh renderer's
3515 /// per-Aplicacao program-emission dispatch fans on at mesh-artifact
3516 /// materialization time (MESH-COMPOSITION §III.1 — the typed graph's
3517 /// vertex set the `:contratos` `:de`/`:para` edges resolve against and
3518 /// the `:entrada :para` external-gateway destination validates
3519 /// against; CAIXA-SDLC §II — the typed-M3 slot algebra the operator's
3520 /// per-Aplicacao fan-out dispatch fans on). Every per-member axis
3521 /// threads through a lifted per-entry accessor on the
3522 /// [`crate::aplicacao::Membro`] type: the
3523 /// [`crate::aplicacao::Membro::nome`] DNS-1123-label member-caixa-
3524 /// identity scalar accessor (4a32abf) and the peer
3525 /// [`crate::aplicacao::Membro::versao_requirement`] SemVer-2
3526 /// version-requirement scalar accessor (a40b0e3). Every downstream
3527 /// consumer of the mesh-graph path first passes through this outer
3528 /// accessor onto the slice and then dispatches per-member through
3529 /// the inner accessors — the two-level dispatch means every per-
3530 /// `:membros` reader now routes through a typed dispatch on the
3531 /// substrate primitive at both altitudes.
3532 ///
3533 /// Prior to this lift the `.membros` `Vec<Membro>` slot was accessed
3534 /// inline at three production sites across two files — the
3535 /// [`Self::declared_mesh_slots`] mesh-slot declared-slot
3536 /// enumerator's `!self.membros.is_empty()` presence probe
3537 /// (caixa-core/src/manifest.rs, which drives the
3538 /// `M3_AUTHOR_KEY_MEMBROS` kebab-case author-label push every
3539 /// [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-coherence
3540 /// gate reads), the [`Self::aplicacao_view`] per-Aplicacao typed-view
3541 /// composer's `self.membros.clone()` per-member fold-in path
3542 /// (caixa-core/src/manifest.rs, which materializes the typed
3543 /// [`crate::aplicacao::AplicacaoSpec`] view every
3544 /// [`crate::StandardLayout::verify`] Aplicacao-arm gate dispatches
3545 /// on), and the [`crate::StandardLayout::verify`] per-`:membros
3546 /// :caixa` self-membership refusal probe's `&caixa.membros`-borrowed
3547 /// [`crate::aplicacao::validate_no_self_membership`] input
3548 /// (caixa-core/src/layout.rs, which pins the "no member names the
3549 /// Aplicacao's own `:nome`" cross-slot coherence gate). A future
3550 /// extension of the outer `:membros` axis (a per-cluster
3551 /// `:membros-overrides` overlay the wasm-engine operator resolves at
3552 /// admission time so a cluster-specific member-set can tighten a
3553 /// caixa-declared list without re-authoring the `caixa.lisp`,
3554 /// promotion of the plain `Vec<Membro>` to a richer
3555 /// `{static, dynamic}` partition once runtime-resolved Aplicacao
3556 /// members land as a typed axis, per-member priority annotation once
3557 /// multi-strategy fan-out lands) would have had to be threaded
3558 /// through all three open-coded copies in lockstep or one consumer
3559 /// would silently disagree with the peers on which member slice a
3560 /// given Caixa resolves to — the enumerator's presence probe reading
3561 /// the raw slot while the peer view-composer's fold-in path read an
3562 /// operator-resolved slot would silently split the paired
3563 /// declared-slot enumerator and typed-view composition, and the
3564 /// [`crate::aplicacao::validate_no_self_membership`] self-membership
3565 /// refusal probe reading a third borrow would silently drift the
3566 /// cross-slot coherence gate's traversal input from the two peers, a
3567 /// three-consumer split at the enumerator, the view composer, and
3568 /// the self-membership gate far from the source `caixa.lisp` with no
3569 /// field naming the member-set-drift root cause. Lifting the
3570 /// resolution rule to a typed method on the substrate primitive
3571 /// means every downstream consumer of the caixa's per-`Caixa`
3572 /// MESH-COMPOSITION outer-slice surface reaches for exactly one
3573 /// typed dispatch — the resolver's accept-set migrates as a unit on
3574 /// any future axis addition.
3575 ///
3576 /// Third outer top-level [`Caixa`] `&[Composite]`-return slice
3577 /// accessor for M2 / M3 typed-slot vec-carry axes — opens the outer-
3578 /// `Caixa` M3 mesh-slot arm of the `&[Composite]` composite-slice
3579 /// sub-family the sibling M2 [`Self::upgrade_from`] (2a1f907) /
3580 /// [`Self::children`] (c17b51e) accessors opened for the M2 vec-carry
3581 /// altitude. Peer at the outer altitude of the closed inner-
3582 /// [`crate::AplicacaoSpec::membros`] (6c77e36) accessor on the same
3583 /// MESH-COMPOSITION per-Aplicacao member-list axis — the two
3584 /// altitudes now share the same "byte-equal, borrow-shared" outer-
3585 /// accessor discipline. Sibling in shape to the peer outer-`Caixa`
3586 /// `&[Dep]`-return [`Self::deps`] (ad34b4e) / [`Self::deps_dev`]
3587 /// (f7fd81e) and `&[String]`-return [`Self::autores`] (b5d813f) /
3588 /// [`Self::etiquetas`] (78c7d3c) / [`Self::bibliotecas`] (8a36c23) /
3589 /// [`Self::exe`] (65d9527) / [`Self::servicos`] (611f78b) slice-
3590 /// accessors on the sibling outer-`Caixa` scalar-element vec-carry
3591 /// axes — folds the "outer [`Caixa`] `&[T]` slice" projection
3592 /// pattern onto the sibling M3 typed-composite-element axis
3593 /// ([`crate::aplicacao::Membro`] composite, matching the per-inner
3594 /// [`crate::AplicacaoSpec::membros`] element type at a different
3595 /// altitude).
3596 ///
3597 /// Returns `&[crate::aplicacao::Membro]` (not `&Vec<Membro>`)
3598 /// because every downstream consumer of the member list treats it
3599 /// as a read-only sequence — the slice-view is the narrowest borrow
3600 /// that supports every present + roadmapped consumer (`.iter()`,
3601 /// `.len()`, `.is_empty()`, the
3602 /// [`crate::aplicacao::validate_no_self_membership`] `&[Membro]`
3603 /// input, `serde` slice-serialization) without leaking the backing
3604 /// `Vec`'s grow/push/reserve surface no consumer of the typed view
3605 /// reaches for (the storage-side `Vec` remains reachable through the
3606 /// `pub membros` field for the mutation-carrying serde round-trip
3607 /// and per-test fixture-mutation paths, including the
3608 /// [`Self::aplicacao_view`] fold-in path that clones the slot into
3609 /// the typed view). Named `membros()` to match the storage field's
3610 /// name verbatim and the tatara-lisp author-surface term
3611 /// (`:membros`) the field's own docstring already carries; the
3612 /// accessor's identity maps onto the canonical MESH-COMPOSITION
3613 /// vocabulary the [`Caixa::membros`] field's docstring already
3614 /// reaches for ("Member Servicos that make up this Aplicacao").
3615 #[must_use]
3616 pub fn membros(&self) -> &[crate::aplicacao::Membro] {
3617 self.membros.as_slice()
3618 }
3619
3620 /// Substrate-canonical per-`Caixa` `:contratos` M3 mesh-slot outer-
3621 /// composite MESH-COMPOSITION-shaped per-Aplicacao WIT-typed
3622 /// inter-Servico contract-list slice accessor every consumer of the
3623 /// top-level manifest's per-Aplicacao `&[crate::aplicacao::WitContract]`
3624 /// slice-view keys off — returns the author-declared `:contratos`
3625 /// typed `Vec<crate::aplicacao::WitContract>` verbatim as a
3626 /// `&[crate::aplicacao::WitContract]` slice-view over the same
3627 /// backing buffer the raw `self.contratos.as_slice()` field access
3628 /// borrows from. Empty-slice-carrying (the "no contracts declared"
3629 /// arm every non-`Aplicacao`-kind `defcaixa` carries by
3630 /// `#[serde(default)]` and every leaf Aplicacao carrying only a
3631 /// single member with no inter-Servico edge carries; the returned
3632 /// `&[WitContract]` degenerates to an empty slice on those arms
3633 /// without any silent `None` collapse).
3634 ///
3635 /// The outer `:contratos` slot carries the M3 typed MESH-COMPOSITION
3636 /// per-Aplicacao WIT-typed inter-Servico edge list — the load-bearing
3637 /// container of every per-edge `{de, para, wit, endpoint | subject |
3638 /// slot}` quadruple the caixa-mesh renderer's per-Aplicacao
3639 /// `CiliumNetworkPolicy` fan-out (one L7 policy per edge —
3640 /// MESH-COMPOSITION §III.2 point 2) and per-`(:de, :para)`
3641 /// adjacency-list seed dispatch on at mesh-artifact materialization
3642 /// time (MESH-COMPOSITION §III.1 — the typed graph's edge set the
3643 /// `:membros` vertex set resolves against, closed by the
3644 /// [`crate::AplicacaoError::ContractoUnknownMember`] / cycle-refusal
3645 /// gates in §III.3; CAIXA-SDLC §II — the typed-M3 slot algebra the
3646 /// operator's per-Aplicacao fan-out dispatch fans on). Every
3647 /// per-edge axis threads through a lifted per-entry accessor on the
3648 /// [`crate::aplicacao::WitContract`] type: the peer `de` / `para`
3649 /// DNS-1123-label member-caixa-name endpoint scalar accessors, the
3650 /// [`crate::aplicacao::WitContract::endpoint`] (7020470) HTTP-shape
3651 /// / [`crate::aplicacao::WitContract::subject`] (90de675)
3652 /// NATS-pub-sub-shape / [`crate::aplicacao::WitContract::slot`]
3653 /// (ed22b66) `wasi:keyvalue/store`-shape payload-carrier accessors,
3654 /// and the WIT-world discriminant. Every downstream consumer of the
3655 /// mesh-graph edge path first passes through this outer accessor
3656 /// onto the slice and then dispatches per-contract through the
3657 /// inner accessors — the two-level dispatch means every
3658 /// per-`:contratos` reader now routes through a typed dispatch on
3659 /// the substrate primitive at both altitudes.
3660 ///
3661 /// Prior to this lift the `.contratos` `Vec<WitContract>` slot was
3662 /// accessed inline at two production sites in
3663 /// caixa-core/src/manifest.rs — the [`Self::declared_mesh_slots`]
3664 /// mesh-slot declared-slot enumerator's
3665 /// `!self.contratos.is_empty()` presence probe (which drives the
3666 /// `M3_AUTHOR_KEY_CONTRATOS` kebab-case author-label push every
3667 /// [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-coherence
3668 /// gate reads) and the [`Self::aplicacao_view`] per-Aplicacao
3669 /// typed-view composer's `self.contratos.clone()` per-contract
3670 /// fold-in path (which materializes the typed
3671 /// [`crate::aplicacao::AplicacaoSpec`] view every
3672 /// [`crate::StandardLayout::verify`] Aplicacao-arm gate and every
3673 /// downstream `caixa-mesh` renderer dispatches on). A future
3674 /// extension of the outer `:contratos` axis (a per-cluster
3675 /// `:contratos-overrides` overlay the wasm-engine operator resolves
3676 /// at admission time so a cluster-specific edge-set can tighten a
3677 /// caixa-declared list without re-authoring the `caixa.lisp`,
3678 /// promotion of the plain `Vec<WitContract>` to a richer
3679 /// `{static, dynamic}` partition once runtime-resolved contract
3680 /// edges land, per-edge policy annotation once the M4 per-edge
3681 /// policy overlay axis lands) would have had to be threaded through
3682 /// both open-coded copies in lockstep or one consumer would
3683 /// silently disagree with the peer on which edge slice a given
3684 /// Caixa resolves to — the enumerator's presence probe reading the
3685 /// raw slot while the peer view-composer's fold-in path read an
3686 /// operator-resolved slot would silently split the paired
3687 /// declared-slot enumerator and typed-view composition, a
3688 /// two-consumer split at the enumerator and the view composer far
3689 /// from the source `caixa.lisp` with no field naming the edge-set-
3690 /// drift root cause. Lifting the resolution rule to a typed method
3691 /// on the substrate primitive means every downstream consumer of
3692 /// the caixa's per-`Caixa` MESH-COMPOSITION outer-slice surface
3693 /// reaches for exactly one typed dispatch — the resolver's
3694 /// accept-set migrates as a unit on any future axis addition.
3695 ///
3696 /// Fourth and final outer top-level [`Caixa`] `&[Composite]`-return
3697 /// slice accessor for M2 / M3 typed-slot vec-carry axes — closes
3698 /// the outer-`Caixa` `&[Composite]` composite-slice sub-family the
3699 /// sibling M2 [`Self::upgrade_from`] (2a1f907) / [`Self::children`]
3700 /// (c17b51e) accessors opened and the M3 [`Self::membros`]
3701 /// (0f26987) accessor folded on, and closes the outer-`Caixa` M3
3702 /// mesh-slot arm of the composite-slice sub-family the sibling
3703 /// [`Self::membros`] accessor opened for the M3 vec-carry altitude.
3704 /// Peer at the outer altitude of the closed inner-
3705 /// [`crate::AplicacaoSpec::contratos`] (0dcc926) accessor on the
3706 /// same MESH-COMPOSITION per-Aplicacao contract-list axis — the two
3707 /// altitudes now share the same "byte-equal, borrow-shared" outer-
3708 /// accessor discipline. Sibling in shape to the peer outer-`Caixa`
3709 /// `&[Dep]`-return [`Self::deps`] (ad34b4e) / [`Self::deps_dev`]
3710 /// (f7fd81e) and `&[String]`-return [`Self::autores`] (b5d813f) /
3711 /// [`Self::etiquetas`] (78c7d3c) / [`Self::bibliotecas`] (8a36c23) /
3712 /// [`Self::exe`] (65d9527) / [`Self::servicos`] (611f78b) slice-
3713 /// accessors on the sibling outer-`Caixa` scalar-element vec-carry
3714 /// axes — folds the "outer [`Caixa`] `&[T]` slice" projection
3715 /// pattern onto the sibling M3 typed-composite-element axis
3716 /// ([`crate::aplicacao::WitContract`] composite, matching the
3717 /// per-inner [`crate::AplicacaoSpec::contratos`] element type at a
3718 /// different altitude).
3719 ///
3720 /// Returns `&[crate::aplicacao::WitContract]` (not
3721 /// `&Vec<WitContract>`) because every downstream consumer of the
3722 /// contract list treats it as a read-only sequence — the slice-view
3723 /// is the narrowest borrow that supports every present + roadmapped
3724 /// consumer (`.iter()`, `.len()`, `.is_empty()`, per-edge WIT-world
3725 /// discriminant dispatch, `serde` slice-serialization) without
3726 /// leaking the backing `Vec`'s grow/push/reserve surface no
3727 /// consumer of the typed view reaches for (the storage-side `Vec`
3728 /// remains reachable through the `pub contratos` field for the
3729 /// mutation-carrying serde round-trip and per-test fixture-mutation
3730 /// paths, including the [`Self::aplicacao_view`] fold-in path that
3731 /// clones the slot into the typed view). Named `contratos()` to
3732 /// match the storage field's name verbatim and the tatara-lisp
3733 /// author-surface term (`:contratos`) the field's own docstring
3734 /// already carries; the accessor's identity maps onto the canonical
3735 /// MESH-COMPOSITION vocabulary the [`Caixa::contratos`] field's
3736 /// docstring already reaches for ("WIT-typed inter-Servico
3737 /// contracts").
3738 #[must_use]
3739 pub fn contratos(&self) -> &[crate::aplicacao::WitContract] {
3740 self.contratos.as_slice()
3741 }
3742
3743 /// Compose the Aplicacao-related flat slots into a single typed
3744 /// [`crate::aplicacao::AplicacaoSpec`] for validation +
3745 /// downstream renderer consumption. Returns `None` when the
3746 /// caixa isn't a `:kind Aplicacao`.
3747 #[must_use]
3748 pub fn aplicacao_view(&self) -> Option<crate::aplicacao::AplicacaoSpec> {
3749 if !self.kind().is_aplicacao() {
3750 return None;
3751 }
3752 Some(crate::aplicacao::AplicacaoSpec {
3753 membros: self.membros().to_vec(),
3754 contratos: self.contratos().to_vec(),
3755 politicas: self.politicas().cloned().unwrap_or_default(),
3756 placement: self.placement().cloned().unwrap_or_default(),
3757 entrada: self.entrada().cloned(),
3758 })
3759 }
3760
3761 /// The kebab-case `:slot` tags of every M3 mesh slot this caixa
3762 /// *declares* a value on, in canonical declaration order
3763 /// (`:membros` → `:contratos` → `:politicas` → `:placement` →
3764 /// `:entrada`). A slot counts as declared when its backing field
3765 /// carries a value — a non-empty `Vec`, or a `Some(...)`.
3766 ///
3767 /// The M3 mesh slots compose the typed graph of a `:kind Aplicacao`
3768 /// (MESH-COMPOSITION §III.1). [`Self::aplicacao_view`] only folds
3769 /// them into a validatable [`crate::aplicacao::AplicacaoSpec`] when
3770 /// the kind matches (returns `None` otherwise), and the caixa-mesh /
3771 /// caixa-flux / caixa-helm renderers only emit them for an
3772 /// Aplicacao. On any *other* kind a declared mesh slot is the
3773 /// manifest field's documented "ignored otherwise" (see the
3774 /// `:membros` … `:entrada` field docs): it silently passes
3775 /// [`Caixa::from_lisp`] and then vanishes — never validated, never
3776 /// rendered — far from the source caixa.lisp.
3777 /// [`crate::StandardLayout::verify`] consults this to reject that
3778 /// silent-drop at caixa-build time
3779 /// ([`crate::LayoutError::MeshSlotsOnNonAplicacao`]), mirroring the
3780 /// `SupervisorOwnsCode` / `AplicacaoOwnsCode` kind-coherence gates:
3781 /// a slot foreign to the kind is a build error, not a silent drop.
3782 ///
3783 /// Lifted as a typed method (rather than an inline disjunction at
3784 /// the verify call site) so the mesh-slot set lives in one place —
3785 /// a future M4 axis added to the Aplicacao surface (per-edge policy
3786 /// overlay, distributed-app takeover config) is one push here, and
3787 /// every consumer reaching for "which mesh slots are set" (the
3788 /// verify gate, a future `feira lint` kind-coherence advisory)
3789 /// inherits the canonical order without rolling its own.
3790 ///
3791 /// Each per-arm kebab-case label is routed through the peer
3792 /// [`crate::M3_AUTHOR_KEY_MEMBROS`] /
3793 /// [`crate::M3_AUTHOR_KEY_CONTRATOS`] /
3794 /// [`crate::M3_AUTHOR_KEY_POLITICAS`] /
3795 /// [`crate::M3_AUTHOR_KEY_PLACEMENT`] /
3796 /// [`crate::M3_AUTHOR_KEY_ENTRADA`] consts declared next to the
3797 /// [`crate::M3_KEY_PLACEMENT`] renderer-side wire-key peer, so both
3798 /// halves of every M3 top-level mesh slot's dual axis (author-facing
3799 /// kebab-case label + renderer-side artifact key) route through one
3800 /// canonical declaration per arm — same discipline the peer
3801 /// [`crate::M2_AUTHOR_KEY_LIMITS`] / [`crate::M2_AUTHOR_KEY_BEHAVIOR`]
3802 /// / [`crate::M2_AUTHOR_KEY_UPGRADE_FROM`] top-level M2 slot consts
3803 /// (f49c8b0) establish on the sibling per-Servico M2 top-level slot
3804 /// axis, extended here to close the M3 mesh-slot author-facing-label
3805 /// axis so both altitudes of the typed-slot algebra
3806 /// (per-Servico M2 + per-Aplicacao M3) share the same
3807 /// "one canonical byte-string per arm, next to the axis" discipline.
3808 #[must_use]
3809 pub fn declared_mesh_slots(&self) -> Vec<&'static str> {
3810 let mut slots = Vec::new();
3811 if !self.membros().is_empty() {
3812 slots.push(crate::render::M3_AUTHOR_KEY_MEMBROS);
3813 }
3814 if !self.contratos().is_empty() {
3815 slots.push(crate::render::M3_AUTHOR_KEY_CONTRATOS);
3816 }
3817 if self.politicas().is_some() {
3818 slots.push(crate::render::M3_AUTHOR_KEY_POLITICAS);
3819 }
3820 if self.placement().is_some() {
3821 slots.push(crate::render::M3_AUTHOR_KEY_PLACEMENT);
3822 }
3823 if self.entrada().is_some() {
3824 slots.push(crate::render::M3_AUTHOR_KEY_ENTRADA);
3825 }
3826 slots
3827 }
3828
3829 /// The kebab-case `:slot` tags of every supervisor-tree slot this
3830 /// caixa *declares* a value on, in canonical declaration order
3831 /// (`:estrategia` → `:max-restarts` → `:restart-window` →
3832 /// `:children`). A slot counts as declared when its backing field
3833 /// carries a value — a `Some(...)`, or a non-empty `Vec`.
3834 ///
3835 /// The supervisor-tree slots compose the typed OTP supervisor of a
3836 /// `:kind Supervisor` (INSPIRATIONS §II.2; the `:estrategia` +
3837 /// `:children` field docs above). [`Self::supervisor_view`] only
3838 /// folds them into a validatable [`SupervisorSpec`] when the kind
3839 /// matches (returns `None` otherwise), and the wasm-operator's
3840 /// hierarchical reconciler only consumes them for a Supervisor. On
3841 /// any *other* kind a declared supervisor slot is the manifest
3842 /// field's documented "ignored otherwise" (see the `:estrategia` …
3843 /// `:children` field docs): it silently passes [`Caixa::from_lisp`]
3844 /// and then vanishes — never validated, never reconciled — far from
3845 /// the source caixa.lisp. [`crate::StandardLayout::verify`] consults
3846 /// this to reject that silent-drop at caixa-build time
3847 /// ([`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]), the
3848 /// exact mirror of the [`Self::declared_mesh_slots`] /
3849 /// [`crate::LayoutError::MeshSlotsOnNonAplicacao`] gate on the
3850 /// Aplicacao-only slot set: a slot foreign to the kind is a build
3851 /// error, not a silent drop.
3852 #[must_use]
3853 pub fn declared_supervisor_slots(&self) -> Vec<&'static str> {
3854 let mut slots = Vec::new();
3855 if self.estrategia().is_some() {
3856 slots.push(crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA);
3857 }
3858 if self.max_restarts().is_some() {
3859 slots.push(crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS);
3860 }
3861 if self.restart_window().is_some() {
3862 slots.push(crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW);
3863 }
3864 if !self.children().is_empty() {
3865 slots.push(crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN);
3866 }
3867 slots
3868 }
3869
3870 /// The kebab-case `:slot` tags of every M2 Servico-runtime slot this
3871 /// caixa *declares* a value on, in canonical declaration order
3872 /// (`:limits` → `:behavior` → `:upgrade-from`). A slot counts as
3873 /// declared when its backing field carries a value — a `Some(...)`,
3874 /// or a non-empty `Vec`.
3875 ///
3876 /// The M2 slots configure the runtime of a long-running wasm
3877 /// component, i.e. a `:kind Servico`: `:limits` is Lunatic
3878 /// per-process sandboxing (INSPIRATIONS §III.1), `:behavior` is the
3879 /// OTP `gen_server` callback set (§II.3), `:upgrade-from` is the OTP
3880 /// appup hot-code-reload table (§II.4). The caixa-helm / caixa-flux
3881 /// renderers gate on [`crate::require_kind`]`(_, Servico)` and only
3882 /// emit these slots for a Servico; on any *other* kind a declared M2
3883 /// slot is the manifest field's documented "ignored otherwise": its
3884 /// well-formedness is checked by [`crate::StandardLayout::verify`]
3885 /// but the value is never rendered into a chart / programs.yaml entry
3886 /// — it silently passes [`Caixa::from_lisp`] + `feira build` and then
3887 /// vanishes, far from the source caixa.lisp.
3888 /// [`crate::StandardLayout::verify`] consults this to reject that
3889 /// silent-drop at caixa-build time
3890 /// ([`crate::LayoutError::ServicoSlotsOnNonServico`]), the exact
3891 /// mirror of the [`Self::declared_mesh_slots`] /
3892 /// [`Self::declared_supervisor_slots`] gates on the peer
3893 /// kind-exclusive slot sets: a slot foreign to the kind is a build
3894 /// error, not a silent drop.
3895 ///
3896 /// Each per-arm kebab-case label is routed through the peer
3897 /// [`crate::M2_AUTHOR_KEY_LIMITS`] / [`crate::M2_AUTHOR_KEY_BEHAVIOR`] /
3898 /// [`crate::M2_AUTHOR_KEY_UPGRADE_FROM`] consts declared next to the
3899 /// [`crate::M2_KEY_LIMITS`] / [`crate::M2_KEY_BEHAVIOR`] /
3900 /// [`crate::M2_KEY_UPGRADE_FROM`] renderer-side wire-key peers, so
3901 /// both halves of the M2 top-level slot's dual axis (author-facing
3902 /// kebab-case label + renderer-side camelCase overlay-container wire
3903 /// key) route through one canonical declaration per arm — same
3904 /// discipline the peer [`crate::M2_BEHAVIOR_AUTHOR_KEY_ON_*`] sub-slot
3905 /// author-label consts (889dc18) establish on the sibling
3906 /// per-callback axis inside the `:behavior` overlay block.
3907 #[must_use]
3908 pub fn declared_servico_slots(&self) -> Vec<&'static str> {
3909 let mut slots = Vec::new();
3910 if self.limits().is_some() {
3911 slots.push(crate::render::M2_AUTHOR_KEY_LIMITS);
3912 }
3913 if self.behavior().is_some() {
3914 slots.push(crate::render::M2_AUTHOR_KEY_BEHAVIOR);
3915 }
3916 if !self.upgrade_from().is_empty() {
3917 slots.push(crate::render::M2_AUTHOR_KEY_UPGRADE_FROM);
3918 }
3919 slots
3920 }
3921
3922 /// The kebab-case `:slot` tags of every code-surface slot this caixa
3923 /// declares a value on that its [`CaixaKind`] doesn't natively own,
3924 /// in canonical declaration order (`:exe` → `:servicos`). A
3925 /// code-surface slot is owned by exactly one kind: `:exe` by
3926 /// [`CaixaKind::Binario`] (the nix-built executable surface), and
3927 /// `:servicos` by [`CaixaKind::Servico`] (the wasm component +
3928 /// `ComputeUnit` daemon surface).
3929 ///
3930 /// Each is silently ignored when declared on the wrong kind: the
3931 /// caixa-helm / caixa-flux / caixa-flake renderers gate on
3932 /// [`crate::require_kind`]`(_, <owning-kind>)`, so on any *other*
3933 /// code-running kind a declared `:exe` / `:servicos` is the manifest
3934 /// field's documented "ignored otherwise" — its path is checked for
3935 /// existence by the layout's `bibliotecas`/`exe`/`servicos` loops
3936 /// (which run after [`Caixa::from_lisp`]), but the value is never
3937 /// rendered into a build target or programs.yaml entry. It silently
3938 /// passes [`Caixa::from_lisp`] + `feira build`, far from the source
3939 /// caixa.lisp, with no field naming which slot is foreign.
3940 ///
3941 /// [`crate::StandardLayout::verify`] consults this to reject that
3942 /// silent-drop at caixa-build time
3943 /// ([`crate::LayoutError::ForeignCodeSlot`]), beside the M2
3944 /// servico-runtime, supervisor-tree, and M3 mesh kind-coherence
3945 /// gates ([`Self::declared_servico_slots`] /
3946 /// [`Self::declared_supervisor_slots`] /
3947 /// [`Self::declared_mesh_slots`]): the fourth kind ↔ slot algebra
3948 /// axis to be closed on the typed surface. The Supervisor /
3949 /// Aplicacao "no code at all" cases ([`crate::LayoutError::SupervisorOwnsCode`]
3950 /// / [`crate::LayoutError::AplicacaoOwnsCode`]) keep their dedicated
3951 /// diagnostics — they fire ahead of this gate on the same `verify`
3952 /// pass, so for Supervisor / Aplicacao the `OwnCode` arm always wins
3953 /// and this method is moot. For Biblioteca / Binario / Servico, this
3954 /// gate fires when a code-running kind declares another code-running
3955 /// kind's exclusive code surface.
3956 ///
3957 /// `:bibliotecas` is deliberately excluded — a Binario or Servico
3958 /// may legitimately ship a `lib/` helper that the underlying
3959 /// substrate (the nix flake for Binario, the wasm component build
3960 /// for Servico) bundles into its build, so the slot's
3961 /// declared-on-wrong-kind cardinality isn't a structural error on
3962 /// either code-running kind. A Biblioteca declaring `:bibliotecas`
3963 /// is the native case (the slot's owning kind). Supervisor /
3964 /// Aplicacao declaring `:bibliotecas` is gated upstream by
3965 /// [`crate::LayoutError::SupervisorOwnsCode`] /
3966 /// [`crate::LayoutError::AplicacaoOwnsCode`].
3967 ///
3968 /// Lifted as a typed method (rather than an inline disjunction at
3969 /// the verify call site) so the foreign-code-slot set lives in one
3970 /// place — a future kind that gains its own code-surface slot is
3971 /// one push here, and every consumer reaching for "which code
3972 /// surfaces are foreign to this kind" (the verify gate, a future
3973 /// `feira lint` kind-coherence advisory, the future `app-operator`'s
3974 /// per-caixa build-target classifier) inherits the canonical order
3975 /// without rolling its own.
3976 #[must_use]
3977 pub fn declared_foreign_code_slots(&self) -> Vec<&'static str> {
3978 let mut slots = Vec::new();
3979 if !self.exe().is_empty() && !self.kind().requires_exe() {
3980 slots.push(":exe");
3981 }
3982 if !self.servicos().is_empty() && !self.kind().requires_servicos() {
3983 slots.push(":servicos");
3984 }
3985 slots
3986 }
3987
3988 /// Validate every entry of `:deps` and `:deps-dev` through
3989 /// [`Dep::validate`] — closing the parity loop with the per-axis
3990 /// `:versao` gates already wired into the typed-graph
3991 /// ([`crate::AplicacaoSpec::validate_membros`] for `:membros`,
3992 /// 9888b13) and typed supervisor tree
3993 /// ([`crate::SupervisorSpec::validate`] for `:children`, b38ff3a).
3994 ///
3995 /// Until this gate landed `:deps :versao` and `:deps-dev :versao`
3996 /// were the only `:versao` axes still untyped past
3997 /// [`Caixa::from_lisp`]: the derive macro stored the requirement
3998 /// as a String without parsing it, so a malformed-but-non-empty
3999 /// requirement (`"^bad-version"`, `"^^0.1"`, `"v0.1"`, `"not-a-req"`)
4000 /// silently passed parse and the `semver::Error` surfaced at
4001 /// lacre-resolve time, far from the source caixa.lisp, with no
4002 /// field naming which `:deps` entry carried the typo. Lifting the
4003 /// gate here makes the four `:versao` typed surfaces (`:deps`,
4004 /// `:deps-dev`, `:membros`, `:children`) structurally equivalent —
4005 /// every requirement string past `validate_deps` is round-trippable
4006 /// through [`crate::parse_requirement`] without re-checking at the
4007 /// resolver layer.
4008 ///
4009 /// Both lists run through the same per-entry validator so a typo
4010 /// in `:deps-dev` surfaces with the same diagnostic as one in
4011 /// `:deps` — neither axis is a second-class citizen of the typed
4012 /// surface.
4013 ///
4014 /// Within each list, [`DepError::DuplicateNome`] closes the
4015 /// set-not-multiset discipline on the `:nome` axis: two entries
4016 /// naming the same caixa carry two `:versao` / `:fonte` / feature
4017 /// triples that the caixa-resolver's lacre pipeline collapses to one
4018 /// via its `HashMap`-keyed-by-`:nome` consumption — the second entry
4019 /// silently overwrites the first at `concrete_versao`-resolve time
4020 /// (the same "second wins / one silently overwrites the other"
4021 /// shape the peer typed-graph duplicate gates already close on every
4022 /// other Vec-shaped authoring surface that keys by name). The
4023 /// duplicate check fires per-list and runs *after* each per-entry
4024 /// [`Dep::validate`] call so a malformed-and-duplicated entry
4025 /// surfaces its narrower per-entry diagnostic
4026 /// ([`DepError::NomeInvalid`], [`DepError::VersaoInvalid`],
4027 /// [`DepError::FonteRepoEmpty`], …) before the cross-entry duplicate
4028 /// diagnostic — the canonical "per-entry shape before cross-entry
4029 /// uniqueness" precedence the peer `:children :caixa`
4030 /// ([`crate::SupervisorSpec::validate`]), `:membros :caixa`
4031 /// ([`crate::AplicacaoSpec::validate_membros`]), `:contratos`
4032 /// ([`crate::AplicacaoSpec::validate`]), `:placement :clusters`
4033 /// ([`crate::AplicacaoSpec::validate_placement`]),
4034 /// `:entrada :paths` ([`crate::AplicacaoSpec::validate`]),
4035 /// `:upgrade-from :from` ([`crate::upgrade::validate_upgrade_from`]),
4036 /// and the within-`:upgrade-from`-entry per-instruction-class
4037 /// singularity gates ([`crate::UpgradeError::DuplicateLoadModule`],
4038 /// [`crate::UpgradeError::DuplicateStateChange`],
4039 /// [`crate::UpgradeError::DuplicateCleanup`]) all establish.
4040 ///
4041 /// Cross-list (`:deps` ↔ `:deps-dev`) coincidence is *not* gated
4042 /// here: Cargo's `[dependencies]` + `[dev-dependencies]` accept the
4043 /// same name in both tables (the dev table's pin overrides the
4044 /// runtime table's pin in test/dev contexts), and caixa's surface
4045 /// mirrors that convention until a deliberate choice retires the
4046 /// override pattern. Only within-list duplicates are structurally
4047 /// incoherent — those are what this gate closes.
4048 pub fn validate_deps(&self) -> Result<(), DepError> {
4049 for &list in crate::dep::DepList::ALL {
4050 let mut seen = std::collections::HashSet::new();
4051 for dep in self.deps_of(list) {
4052 dep.validate()?;
4053 crate::render::insert_first_seen(&mut seen, dep.nome(), || {
4054 DepError::DuplicateNome {
4055 nome: dep.nome().to_string(),
4056 list: list.as_str(),
4057 }
4058 })?;
4059 }
4060 }
4061 Ok(())
4062 }
4063
4064 /// Reject `:nome` values the K8s apiserver would refuse at admission
4065 /// time. The top-level Caixa identity flows directly into every
4066 /// substrate-side artifact's `metadata.name` axis: the
4067 /// `lareira-<nome>` Helm chart name ([`caixa-helm::lib::chart_name`]),
4068 /// the programs.yaml `name:` entry the `lareira-fleet-programs`
4069 /// aggregator keys ComputeUnit derivation off
4070 /// ([`caixa-flux::lib::programs_yaml_entry`]), the
4071 /// `LABEL_APLICACAO` label value carried on every Aplicacao-owned
4072 /// pod and the per-`:contratos` CiliumNetworkPolicy `metadata.name`
4073 /// (`<aplicacao>-<de>-to-<para>`) and the per-`:entrada`
4074 /// `<aplicacao>-<para>` HTTPRoute `metadata.name`
4075 /// ([`caixa-mesh::lib::cilium_network_policies`],
4076 /// [`caixa-mesh::lib::gateway_routes`]), and the default
4077 /// `lib/<nome>.lisp` / `exe/<nome>` layout paths
4078 /// ([`crate::StandardLayout::verify`]). Each K8s apiserver-side
4079 /// schema enforces the DNS-1123 label rule on admission; a
4080 /// structurally invalid `:nome` (`"MyApp"` — the canonical
4081 /// "I copied the display name verbatim" footgun, `"my_app"` — the
4082 /// Python-/Postgres-leak, `"team.app"` — `:nome` is a single label
4083 /// not a subdomain, `"-app"` / `"app-"` — DNS-1123 boundary
4084 /// violations, `"my app"` — the paste-from-doc footgun, `"café"` —
4085 /// IDN must be pre-encoded as Punycode, the 64-byte UUID-shaped
4086 /// over-cap slug) silently passed [`Caixa::from_lisp`] and the
4087 /// failure surfaced at `kubectl apply` time as a `metadata.name:
4088 /// Invalid value` rejection on whichever derived artifact admitted
4089 /// first, far from the source `caixa.lisp` and without any field
4090 /// naming the offending `:nome`.
4091 ///
4092 /// Thin wrapper around [`crate::render::is_dns_1123_label`] (the
4093 /// substrate-side predicate the per-axis name gates already share:
4094 /// `:membros :caixa` 3f9d7a0, `:placement :clusters` 6cbb900,
4095 /// `:children :caixa` 31bfa43) that maps the shared parser-shaped
4096 /// reason into the [`ManifestError::NomeInvalid`] variant, so the
4097 /// diagnostic is self-locating (the offending `:nome` is named
4098 /// verbatim) and the author can grep their `caixa.lisp` for
4099 /// `:nome "<value>"` and fix it in one edit. Same diagnostic shape
4100 /// every per-axis sibling gate already exposes
4101 /// ([`crate::AplicacaoError::MembroCaixaInvalid`],
4102 /// [`crate::AplicacaoError::PlacementClusterInvalid`],
4103 /// [`crate::SupervisorError::ChildCaixaInvalid`]).
4104 ///
4105 /// Empty `:nome` (which [`Caixa::from_lisp`] does not reject — the
4106 /// derive macro stores the raw String) is gated by the narrower
4107 /// [`ManifestError::NomeEmpty`] arm before the predicate is
4108 /// consulted, mirroring the empty-first cascade every per-axis
4109 /// name gate already uses (e.g. `MembroCaixaEmpty` before
4110 /// `MembroCaixaInvalid`, `EmptyChildName` before `ChildCaixaInvalid`).
4111 pub fn validate_nome(&self) -> Result<(), ManifestError> {
4112 // Routes through the shared
4113 // [`crate::render::require_valid_dns_1123_label`] gate the peer
4114 // name axes each land on so drift between the eight axes'
4115 // accepted DNS-1123-label sets is structurally impossible.
4116 let nome = self.nome();
4117 crate::render::require_valid_dns_1123_label(
4118 nome,
4119 || ManifestError::NomeEmpty,
4120 |reason| ManifestError::NomeInvalid {
4121 nome: nome.to_string(),
4122 reason,
4123 },
4124 )
4125 }
4126
4127 /// Reject `:nome` values whose joint length with the canonical
4128 /// [`crate::LAREIRA_CHART_NAME_PREFIX`] (`"lareira-"`) overflows
4129 /// the K8s DNS-1123 label cap [`crate::DNS_1123_LABEL_MAX_LEN`]
4130 /// (63 bytes). Every per-Servico / per-Aplicacao renderer the
4131 /// substrate carries materializes the caixa's `:nome` through the
4132 /// canonical [`crate::lareira_chart_name`] helper (f7320d7) into a
4133 /// `lareira-<nome>` artifact that lands as a K8s `metadata.name` /
4134 /// Helm chart name / `HelmRelease` `release_name`: `caixa-helm`'s
4135 /// `ChartDir.name` + `Chart.yaml::name`
4136 /// (caixa-helm/src/lib.rs:207), `caixa-flux`'s `cluster_bundle`
4137 /// `HelmRelease` `chart:` slot (caixa-flux/src/lib.rs:329),
4138 /// `caixa-tatara`'s `process_for_aplicacao` `release_name` +
4139 /// `oci://<registry>/lareira-<nome>` chart ref
4140 /// (caixa-tatara/src/lib.rs:124,178). Helm's own `Chart.yaml::name`
4141 /// admission rule strict-parses against DNS-1123-label, the Helm
4142 /// operator's tracking-secret name is derived from `release_name`
4143 /// and is itself DNS-1123-label-bounded, and the rendered chart's
4144 /// K8s object `metadata.name` axes embed the chart name as a
4145 /// prefix — every one fails admission on a > 63-byte chart name.
4146 ///
4147 /// The per-axis [`Self::validate_nome`] gate (6c992f8) already
4148 /// caps `:nome` itself at 63 bytes via [`is_dns_1123_label`], so a
4149 /// `:nome` of 56–63 bytes silently passed validate (the inner
4150 /// DNS-1123 check accepts the bare `:nome`) but produced a
4151 /// `lareira-<nome>` of 64–71 bytes that the apiserver / `helm lint`
4152 /// rejected at admission — far from the source `caixa.lisp`, with
4153 /// no field naming the overflow root cause. The
4154 /// [`lareira_chart_name`] helper's own doc comment
4155 /// (caixa-core/src/render.rs:3198) explicitly deferred the fix:
4156 /// "the M4 admission webhook will pin the joint-length invariant
4157 /// when it lands". This gate lands the invariant at the
4158 /// manifest-validate layer rather than waiting for the apiserver
4159 /// — the same fail-at-the-source posture every peer per-axis
4160 /// value-shape gate (DNS-1123 on `:nome`, SemVer-2 on `:versao`,
4161 /// SPDX-expression-shape on `:licenca`, 4-digit decimal year on
4162 /// `:edicao`, etc.) takes.
4163 ///
4164 /// Thin wrapper around
4165 /// [`crate::render::is_lareira_chart_name_shape`] (the
4166 /// substrate-side predicate that composes [`lareira_chart_name`] +
4167 /// [`is_dns_1123_label`] via the lifted
4168 /// [`crate::LAREIRA_CHART_NAME_NOME_MAX_LEN`] budget); maps the
4169 /// shared parser-shaped reason into the
4170 /// [`ManifestError::NomeChartNameBudgetExceeded`] variant so the
4171 /// diagnostic is self-locating (the offending `:nome` is named
4172 /// verbatim alongside the rendered chart name and the budget) and
4173 /// the author can shorten in one edit. The gate runs across every
4174 /// `:kind` — `:nome` is the substrate-wide identity axis any
4175 /// future renderer the substrate adds can derive a
4176 /// `lareira-<nome>` artifact from, and uniform enforcement closes
4177 /// the drift footgun where a future kind grows a chart-emitting
4178 /// render path while the validate cascade doesn't catch it.
4179 ///
4180 /// Runs *after* [`Self::validate_nome`] so the narrower
4181 /// `NomeEmpty` / `NomeInvalid` shape diagnostics fire first — a
4182 /// structurally-malformed `:nome` (empty, uppercase, underscore,
4183 /// dot, leading/trailing hyphen, Unicode, > 63 bytes) surfaces its
4184 /// specific shape error rather than the chart-name-budget error,
4185 /// preserving the legitimate "well-shaped `:nome` that happens to
4186 /// overflow the joint cap" arm for this gate.
4187 pub fn validate_nome_chart_name_budget(&self) -> Result<(), ManifestError> {
4188 let nome = self.nome();
4189 crate::render::is_lareira_chart_name_shape(nome).map_err(|reason| {
4190 ManifestError::NomeChartNameBudgetExceeded {
4191 nome: nome.to_string(),
4192 reason,
4193 }
4194 })
4195 }
4196
4197 /// Reject `:versao` values that don't parse as [`semver::Version`].
4198 /// The top-level Caixa version flows directly into every
4199 /// substrate-side artifact that carries a "this is which version of
4200 /// the caixa" axis: the `lareira-<nome>` Helm chart's `Chart.yaml`
4201 /// `version:` + `appVersion:` axes ([`caixa-helm::lib`] —
4202 /// SemVer-2-strict at `helm template` / `helm install` time per
4203 /// https://helm.sh/docs/topics/charts/#charts-and-versioning), the
4204 /// `feira publish` Zig-style `v<versao>` git tag
4205 /// ([`caixa-flux::lib::programs_yaml_entry`] / the
4206 /// `caixa-publish.yml` reusable workflow), the programs.yaml entry's
4207 /// `versao:` value the `lareira-fleet-programs` aggregator carries
4208 /// onto each rendered ComputeUnit, the OCI image's `:v<versao>` /
4209 /// `:latest` tags the substrate's `wasi-service-flake` builds with
4210 /// `skopeo push`, the lacre closure's pinned versions
4211 /// ([`caixa-resolver`] keys `concrete_versao`), and the
4212 /// `:upgrade-from :from` references peers in this exact `versao`
4213 /// shape (`semver::Version`, not `VersionReq`). Each consumer
4214 /// expects a strict three-part `MAJOR.MINOR.PATCH` (optionally
4215 /// `-prerelease` and/or `+build`); a structurally invalid `:versao`
4216 /// (`"0.1"` — missing patch, the canonical "I shortened it" footgun;
4217 /// `"v0.1.0"` — the git-tag-shape-leaking-into-versao typo;
4218 /// `"latest"` / `"main"` — the "I confused it with a docker tag"
4219 /// footgun; `"^0.1"` / `"~0.1.2"` — the requirement-shape leaking
4220 /// into the version field a peer `:deps :versao` accepts;
4221 /// `"0.1.0.0"` — the four-part Java/Microsoft convention DNS
4222 /// SemVer-2 forbids) silently passed [`Caixa::from_lisp`] (the
4223 /// derive macro stores the raw String) and the failure surfaced at
4224 /// the *first* downstream consumer that strict-parses it: at
4225 /// `helm install` time as a chart-version rejection, at
4226 /// `feira publish` time as a malformed git tag, at lacre-resolve
4227 /// time as a `semver::Error` not naming the offending caixa, at
4228 /// `feira upgrade --to <versao>` time as an unresolvable
4229 /// `:upgrade-from :from` match — far from the source `caixa.lisp`
4230 /// and without any field naming the offending `:versao`.
4231 ///
4232 /// Thin wrapper around [`semver::Version::parse`] — the same parser
4233 /// [`crate::CaixaVersion::parse`] (the typed `:versao` accessor)
4234 /// and [`crate::UpgradeFromEntry::validate`] (the peer
4235 /// `:upgrade-from :from` axis, 26da2c7) consume. Maps the
4236 /// `semver::Error` reason into the [`ManifestError::VersaoInvalid`]
4237 /// variant, carrying the offending `:versao` verbatim + a
4238 /// parser-shaped reason naming the specific violation, so the
4239 /// diagnostic is self-locating (the author can grep their
4240 /// `caixa.lisp` for `:versao "<value>"` and fix it in one edit).
4241 /// Same diagnostic shape as [`ManifestError::NomeInvalid`]
4242 /// (6c992f8) and [`crate::UpgradeError::FromInvalid`]
4243 /// (b0c8389) on the peer axes. With this gate, the typed `:versao`
4244 /// surfaces — top-level `:versao`, `:upgrade-from :from` — are
4245 /// now structurally equivalent (every value past validate is
4246 /// round-trippable through [`semver::Version::parse`] without
4247 /// re-checking at the renderer, resolver, or operator hot-upgrade
4248 /// layer), peer with the four `:versao` requirement axes (`:deps`,
4249 /// `:deps-dev`, `:membros`, `:children`) the prior commits
4250 /// (2420c44, 9888b13, b38ff3a) wired through `parse_requirement`.
4251 ///
4252 /// Empty `:versao` (which [`Caixa::from_lisp`] does not reject —
4253 /// the derive macro stores the raw String) is gated by the
4254 /// narrower [`ManifestError::VersaoEmpty`] arm before the parser is
4255 /// consulted, mirroring the empty-first cascade every per-axis
4256 /// version gate already uses (e.g. `MembroVersaoEmpty` before
4257 /// `MembroVersaoInvalid`, `EmptyChildVersion` before
4258 /// `ChildVersaoInvalid`, `NomeEmpty` before `NomeInvalid`).
4259 pub fn validate_versao(&self) -> Result<(), ManifestError> {
4260 let versao = self.versao();
4261 if versao.is_empty() {
4262 return Err(ManifestError::VersaoEmpty);
4263 }
4264 semver::Version::parse(versao).map_err(|e| ManifestError::VersaoInvalid {
4265 versao: versao.to_string(),
4266 reason: e.to_string(),
4267 })?;
4268 Ok(())
4269 }
4270
4271 /// Reject `:restart-window` values the shared
4272 /// [`crate::supervisor::duration_codec::parse`] refuses. The flat
4273 /// `restart_window: Option<String>` slot on [`Caixa`] is stored
4274 /// raw by the derive macro (the typed [`SupervisorSpec`] holds an
4275 /// `Option<Duration>` routed through the shared codec via `with =
4276 /// "duration_codec"`); the inline `Caixa → SupervisorSpec`
4277 /// view-construction path ([`Self::supervisor_view`]) folds the
4278 /// raw string through the same shared codec and soft-swallows the
4279 /// parse error as `None` to keep the view best-effort. Without
4280 /// this gate a malformed `:restart-window` (`"1.5s"` — the
4281 /// fractional-seconds drift class; `"1.0s"` — the decimal-shaped
4282 /// integer drift; `"0.5m"` — the unit-fraction drift; `"+30s"` /
4283 /// `"-30s"` — the leading-sign drift; `"30x"` — the unknown-unit
4284 /// footgun; `"abc"` — pure garbage; `""` — the empty-after-trim
4285 /// edge case) silently produced a `SupervisorSpec` with
4286 /// `restart_window: None`, indistinguishable from the canonical
4287 /// "omit the slot to express no reset" authoring shape — Erlang/OTP's
4288 /// `MaxIntensity / Period` invariant turns into a never-reset
4289 /// supervisor far from the source `caixa.lisp`, with no field
4290 /// naming the offending `:restart-window`. Lifting the gate to a
4291 /// Caixa-level validator mirrors the trajectory of the peer
4292 /// per-axis identity gates ([`Self::validate_nome`] 6c992f8,
4293 /// [`Self::validate_versao`] 1fdaa02, [`Self::validate_deps`]
4294 /// a7f0d8c) and the ABSORPTION-ROADMAP.md M2.2 test pin
4295 /// (line 196: "reject invalid `:restart-window` (non-duration)").
4296 ///
4297 /// Thin wrapper around [`crate::supervisor::duration_codec::parse`]
4298 /// (the shared codec backing `:supervisor :restart-window` as
4299 /// serde-routed on [`SupervisorSpec`], `:politicas :timeout`, and
4300 /// `:politicas :circuit-breaker :window` — all three covered by
4301 /// the integer-magnitude gate 1c55a2a). Maps the codec's parse
4302 /// error verbatim into the [`ManifestError::RestartWindowMalformed`]
4303 /// variant, carrying the offending raw string + a parser-shaped
4304 /// reason naming the canonical authoring form, so the diagnostic
4305 /// is self-locating (the author can grep their `caixa.lisp` for
4306 /// `:restart-window "<value>"` and fix it in one edit) and
4307 /// uniform with every other manifest-level validate diagnostic.
4308 /// With this gate the four `:restart-window`-shaped surfaces (the
4309 /// flat raw string on [`Caixa`], the typed `Option<Duration>` on
4310 /// [`SupervisorSpec`], the two `MeshPolicy` peer durations) are
4311 /// now structurally equivalent — every value past the codec is in
4312 /// one accepted set, by construction.
4313 ///
4314 /// `None` (the canonical "omit the slot to express no reset"
4315 /// shape) is accepted trivially — the gate is a no-op when the
4316 /// author didn't author a window. The empty string is rejected by
4317 /// the shared codec (its digit-only gate refuses an empty
4318 /// magnitude), surfacing the same `RestartWindowMalformed`
4319 /// diagnostic as every other rejected non-canonical shape.
4320 pub fn validate_restart_window(&self) -> Result<(), ManifestError> {
4321 let Some(s) = self.restart_window() else {
4322 return Ok(());
4323 };
4324 crate::supervisor::duration_codec::parse(s)
4325 .map(|_| ())
4326 .map_err(|reason| ManifestError::RestartWindowMalformed {
4327 restart_window: s.to_string(),
4328 reason,
4329 })
4330 }
4331
4332 /// Reject per-entry values on the three Caixa-level code-surface
4333 /// path lists (`:bibliotecas`, `:exe`, `:servicos`) that the
4334 /// layout checker's `root.join(p)` sandbox would silently subvert.
4335 /// Same three structural footguns the peer
4336 /// [`BehaviorSpec::validate`] (b0c8389) and
4337 /// [`crate::UpgradeInstruction::validate`] `StateChange` arm
4338 /// (26da2c7) already close on the M2 `:behavior :on-*` and
4339 /// `:upgrade-from :state-change :script` axes, here lifted onto
4340 /// the three top-level code-path axes through the shared
4341 /// [`is_sandboxed_relative_path`] predicate:
4342 ///
4343 /// - empty entry (`(:bibliotecas (""))` / `(:exe (""))` /
4344 /// `(:servicos (""))`): `PathBuf::new()` round-trips through
4345 /// [`Path::join`] as the base itself — `root.join("")` ==
4346 /// `root`, so the existence check (`self.exists(&root)`)
4347 /// trivially passes (the project root exists), and the layout
4348 /// silently treats the project root as a biblioteca / exe /
4349 /// servico entry. The `:bibliotecas` loop then hands the root
4350 /// to `tatara_lisp::read` at `feira build` time as if the root
4351 /// directory itself were a Lisp source file — a parse error
4352 /// far from the source `caixa.lisp` with no field naming the
4353 /// offending entry.
4354 /// - absolute path (`(:bibliotecas ("/etc/passwd"))`):
4355 /// [`Path::join`] *replaces* the base when the right-hand side
4356 /// is absolute, so `root.join("/etc/passwd")` resolves to
4357 /// `"/etc/passwd"` and escapes the project sandbox entirely.
4358 /// The existence check then silently consults whatever the
4359 /// escaped path resolves to — for `:bibliotecas`, the layout
4360 /// has no `starts_with`-fence (only `:exe` is fenced under
4361 /// `exe/` and `:servicos` under `servicos/`), so an absolute
4362 /// `:bibliotecas` entry that happens to resolve on disk
4363 /// silently passes. For `:exe` / `:servicos` the fence catches
4364 /// the absolute case downstream as `ExeOutsideDir` /
4365 /// `ServicoOutsideDir` (or `MissingEntry` if the absolute path
4366 /// doesn't exist), but with a downstream-shaped diagnostic
4367 /// that names the resolved escape path rather than the
4368 /// authoring footgun at the source.
4369 /// - parent-escape (`(:bibliotecas ("../sibling/x.lisp"))` /
4370 /// `(:exe ("exe/../../escape.lisp"))`): a [`PathBuf`] with any
4371 /// [`std::path::Component::ParentDir`] anywhere round-trips
4372 /// through [`Path::join`] as a traversal above the caixa root.
4373 /// The `:exe` / `:servicos` `starts_with(<dir>)` fence is
4374 /// *component-aware* (not canonical-path-aware), so
4375 /// `root.join("exe/../../escape.lisp")` `starts_with(exe_dir)`
4376 /// is **true** even though the canonical resolution
4377 /// `{parent of root}/escape.lisp` lives outside the caixa root
4378 /// — the fence silently lets the parent-escape through, and
4379 /// the existence check passes if that escape-target happens
4380 /// to exist. Caught regardless of where the `..` sits
4381 /// (leading, mid-path, trailing) so the gate matches the peer
4382 /// predicate's full coverage.
4383 ///
4384 /// Same `Empty` → `Absolute` → `ParentEscape` arm-ordering every peer
4385 /// `is_sandboxed_relative_path` consumer follows (b0c8389 / 26da2c7);
4386 /// same per-slot diagnostic shape every peer per-axis path-gate
4387 /// exposes (`*Empty { slot }` / `*Absolute { slot, path }` /
4388 /// `*ParentEscape { slot, path }`). Cross-slot precedence is
4389 /// `:bibliotecas` → `:exe` → `:servicos` — the same declaration
4390 /// order [`Caixa::declared_foreign_code_slots`] uses for its
4391 /// canonical foreign-code-slot diagnostic, so a manifest with
4392 /// multiple malformed slots surfaces the lexicographically-earliest
4393 /// slot's diagnostic deterministically.
4394 ///
4395 /// Lifted to the typed surface as a Caixa-level validator (peer
4396 /// of [`Self::validate_nome`] / [`Self::validate_versao`] /
4397 /// [`Self::validate_deps`] / [`Self::validate_restart_window`])
4398 /// and wired into [`crate::StandardLayout::verify`] before the
4399 /// existence-check loops so the diagnostic names the offending
4400 /// slot at the source caixa.lisp rather than reporting a
4401 /// downstream `MissingEntry` / `ExeOutsideDir` /
4402 /// `ServicoOutsideDir` against the resolved sandbox-escape path.
4403 /// The fourth typed code-path surface — every author-supplied
4404 /// path on the manifest — is now structurally accept-shaped
4405 /// past validate, peer with `:behavior :on-*` and
4406 /// `:upgrade-from :state-change :script`.
4407 pub fn validate_code_paths(&self) -> Result<(), ManifestError> {
4408 /// Per-slot file-type contract for the three Caixa-level
4409 /// code-path surfaces (`:bibliotecas`, `:exe`, `:servicos`).
4410 /// Each variant names the predicate the per-entry file-type
4411 /// gate consults; [`Self::None`] opts the slot out of any
4412 /// file-type contract. Lifted as a typed local enum so the
4413 /// per-slot dispatch is exhaustive at the `match` — adding a
4414 /// future axis to the typed-substrate `:` slot set (the
4415 /// future `:assets` resource axis the M5 roadmap names, the
4416 /// future `:nix-flake` derivation axis the caixa-flake
4417 /// emitter consults) lands as one variant + one `match` arm,
4418 /// not a coordinated rewrite of every per-slot bool flag.
4419 ///
4420 /// Peer of the typed-substrate per-slot variant disciplines
4421 /// already established on this surface
4422 /// ([`crate::supervisor::RestartStrategy`] +
4423 /// [`crate::supervisor::RestartPolicy`] on the OTP-shape
4424 /// supervision-tree axis,
4425 /// [`crate::aplicacao::PlacementStrategy`] on the §III.1
4426 /// placement axis, [`crate::aplicacao::WitTarget`] on the
4427 /// `:contratos` payload-target axis): the typed `enum` is
4428 /// the substrate's single source of truth for the per-axis
4429 /// dispatch, and every consumer (the per-arm body here, the
4430 /// future feira-lint per-slot diagnostic renderer, the M4
4431 /// per-axis admission webhook) reaches for the same typed
4432 /// surface rather than re-deriving the partition from inline
4433 /// flag combinations.
4434 enum CodePathFileType {
4435 /// `:exe` — nix-build derivation output, no terminating-
4436 /// extension contract (the canonical `"exe/<name>"`
4437 /// fixtures the layout's `ExeOutsideDir` error message
4438 /// documents carry no extension by convention).
4439 None,
4440 /// `:bibliotecas` — tatara-lisp source files the
4441 /// `feira build` loop reads through `tatara_lisp::read`
4442 /// at parse time. Routes to [`is_lisp_extension`].
4443 LispSource,
4444 /// `:servicos` — ComputeUnit-CR YAML files the
4445 /// caixa-helm / caixa-flux renderers consume through
4446 /// `serde_yaml::from_str`. Routes to
4447 /// [`is_computeunit_yaml_extension`].
4448 ComputeUnitYaml,
4449 }
4450
4451 // The per-slot [`CodePathFileType`] selects which axes carry the
4452 // lifted file-type predicate. `:bibliotecas` is the tatara-lisp
4453 // source axis (the `feira build` loop at
4454 // `caixa-feira/src/cmd/build.rs:33` reads each entry through
4455 // `tatara_lisp::read` at parse time) — the lifted
4456 // [`is_lisp_extension`] predicate gates the `.lisp` extension.
4457 // `:exe` is the nix-built executable surface (per the canonical
4458 // `"exe/<name>"`-shaped fixtures the layout's `ExeOutsideDir`
4459 // error message documents and every in-tree
4460 // `caixa_with_code_paths` positive control uses) — its file-type
4461 // contract is "nix-build derivation output", not a typed source
4462 // file, so [`CodePathFileType::None`] opts the slot out of any
4463 // file-type gate. `:servicos` is the `.computeunit.yaml`
4464 // ComputeUnit-CR axis (the peer caixa-helm / caixa-flux
4465 // renderers consume each entry through `serde_yaml::from_str` as
4466 // a typed `ComputeUnit` CR) — the lifted
4467 // [`is_computeunit_yaml_extension`] predicate gates the compound
4468 // `.computeunit.yaml` suffix. All three axes are surfaced through
4469 // the same iteration so the sandbox-shape + duplicate gates
4470 // apply uniformly; the typed file-type dispatch fires per-slot
4471 // exactly where the downstream consumer's accepted set demands
4472 // it. The third file-type variant ([`ComputeUnitYaml`]) is the
4473 // compounding lift on the peer 64772a9 `:bibliotecas`
4474 // `.lisp`-gate trajectory — the second of the three code-path
4475 // axes to land on a typed compound-suffix gate, with the same
4476 // self-locating per-slot diagnostic shape every peer per-axis
4477 // file-type lift uses (`*NonLispExtension { slot, path }` /
4478 // `*NonComputeUnitYamlExtension { slot, path }`).
4479 for (slot, list, file_type) in [
4480 (
4481 ":bibliotecas",
4482 &self.bibliotecas,
4483 CodePathFileType::LispSource,
4484 ),
4485 (":exe", &self.exe, CodePathFileType::None),
4486 (
4487 ":servicos",
4488 &self.servicos,
4489 CodePathFileType::ComputeUnitYaml,
4490 ),
4491 ] {
4492 // Per-slot set-not-multiset gate on the typed code-path axis.
4493 // Every peer Vec-shaped author-supplied list past validate is
4494 // a set, not a multiset: `:membros :caixa`
4495 // ([`crate::AplicacaoError::MembroDuplicate`]), `:placement
4496 // :clusters` ([`crate::AplicacaoError::PlacementClusterDuplicate`]),
4497 // `:entrada :paths` ([`crate::AplicacaoError::EntradaPathDuplicate`]),
4498 // `:contratos` ([`crate::AplicacaoError::ContratoDuplicate`]),
4499 // `:children :caixa` ([`crate::SupervisorError::DuplicateChild`]),
4500 // `:deps` / `:deps-dev` `:nome` ([`crate::DepError::DuplicateNome`]
4501 // per 359fba5), `:upgrade-from :from` ([`crate::UpgradeError::DuplicateFrom`]),
4502 // `:etiquetas` ([`ManifestError::EtiquetaDuplicate`] per 360a499),
4503 // `:autores` ([`ManifestError::AutorDuplicate`] per 86c769b) —
4504 // the three code-path lists are the last Vec-shaped author-
4505 // supplied slots on the typed Caixa surface still admitting a
4506 // duplicate entry silently. Scope is per-list (`:bibliotecas`
4507 // duplicates are flagged within `:bibliotecas`, not across
4508 // `:bibliotecas` ↔ `:exe`) — the same per-list scope `:deps`
4509 // ↔ `:deps-dev` use (a `:nome` present in both lists is a
4510 // legitimate dev-vs-runtime shape on the dep axis, fenced
4511 // separately by [`crate::dep::validate_no_self_dep`]). On the
4512 // code-path axis a cross-slot collision is structurally
4513 // impossible by the layout's `starts_with(<exe|servicos>_dir)`
4514 // fence — `:exe` and `:servicos` entries are confined to their
4515 // own directory trees, so the only way a string could appear
4516 // on two code-path lists is the (rare, structurally invalid)
4517 // case where `:bibliotecas` carries an `"exe/<x>"` or
4518 // `"servicos/<x>.yaml"`-shaped path.
4519 //
4520 // Without the gate three authoring footguns silently passed:
4521 //
4522 // - `:bibliotecas ("lib/foo.lisp" "lib/foo.lisp")` — the
4523 // canonical copy-paste-the-wrong-file footgun. `feira
4524 // build` (`caixa-feira/src/cmd/build.rs:33`) walks the
4525 // list and re-parses the same file twice, wasting work
4526 // and silently masking the author's intent to declare a
4527 // *second* biblioteca.
4528 // - `:exe ("exe/cli" "exe/cli")` — the same footgun on the
4529 // Binario surface. The future `caixa-flake` `nix flake`
4530 // emitter that materializes each `:exe` entry as a flake
4531 // `packages.<exe-name>` derivation would collide on the
4532 // duplicate package name and surface a flake-eval error
4533 // far from the source `caixa.lisp`.
4534 // - `:servicos ("servicos/x.computeunit.yaml"
4535 // "servicos/x.computeunit.yaml")` — the same footgun on
4536 // the Servico surface. The peer `caixa-helm` / `caixa-flux`
4537 // renderers already refuse `:servicos.len() != 1` with
4538 // the narrower [`UnsupportedServicoCount`] diagnostic, but
4539 // that diagnostic surfaces "too many servicos" without
4540 // naming "duplicate entry" — the typed self-locating
4541 // "which entry is the duplicate" framing only lands at
4542 // this gate.
4543 //
4544 // Same `seen.insert(entry.as_str())` shape every peer per-list
4545 // duplicate gate uses (`:etiquetas` 360a499, `:autores`
4546 // 86c769b, `:deps` 359fba5) and the same "structural shape
4547 // checks fire before the duplicate check on the same entry"
4548 // ordering (a `(:bibliotecas ("" "lib/x.lisp" "lib/x.lisp"))`
4549 // shape surfaces the narrower [`Self::CodePathEmpty`] for the
4550 // empty entry first, not the duplicate on the later pair).
4551 let mut seen = std::collections::HashSet::new();
4552 for entry in list {
4553 let path = Path::new(entry);
4554 match is_sandboxed_relative_path(path) {
4555 Ok(()) => {}
4556 Err(PathShapeViolation::Empty) => {
4557 return Err(ManifestError::CodePathEmpty { slot });
4558 }
4559 Err(PathShapeViolation::Absolute) => {
4560 return Err(ManifestError::CodePathAbsolute {
4561 slot,
4562 path: path.to_path_buf(),
4563 });
4564 }
4565 Err(PathShapeViolation::ParentEscape) => {
4566 return Err(ManifestError::CodePathParentEscape {
4567 slot,
4568 path: path.to_path_buf(),
4569 });
4570 }
4571 }
4572 // The per-slot file-type gate dispatched through the
4573 // typed [`CodePathFileType`] selector above. Each variant
4574 // routes to the lifted predicate the downstream consumer
4575 // demands:
4576 //
4577 // - [`LispSource`] → [`is_lisp_extension`] for
4578 // `:bibliotecas` (the `feira build` loop's
4579 // `tatara_lisp::read` consumer);
4580 // - [`ComputeUnitYaml`] → [`is_computeunit_yaml_extension`]
4581 // for `:servicos` (the caixa-helm / caixa-flux
4582 // `serde_yaml::from_str` consumer's `ComputeUnit` CR
4583 // accepted set);
4584 // - [`None`] for `:exe` — the nix-build derivation-
4585 // output axis has no terminating-extension contract.
4586 //
4587 // Fires after the sandbox-shape arms so a path that is
4588 // *both* sandbox-escaping and wrong-extension surfaces
4589 // the more fundamental sandbox-shape diagnostic first
4590 // (mirrors the peer `EmptyPath` → `AbsolutePath` →
4591 // `ParentEscape` → `NonLispExtension` arm-ordering on
4592 // `:behavior :on-*` c97815a, and `EmptyScript` →
4593 // `AbsoluteScript` → `ParentEscapeScript` →
4594 // `NonLispExtensionScript` on
4595 // `:upgrade-from :state-change :script` 33cc830), and
4596 // before the duplicate gate so the narrower per-entry
4597 // file-type shape dominates the cross-entry uniqueness
4598 // diagnostic (a
4599 // `("servicos/x.yaml" "servicos/x.yaml")` shape on
4600 // `:servicos` surfaces
4601 // `CodePathNonComputeUnitYamlExtension` on the first
4602 // entry rather than `CodePathDuplicate` on the pair —
4603 // peer with the 64772a9 `:bibliotecas`
4604 // `("lib/x.txt" "lib/x.txt")` ordering).
4605 match file_type {
4606 CodePathFileType::None => {}
4607 CodePathFileType::LispSource => {
4608 if !is_lisp_extension(path) {
4609 return Err(ManifestError::CodePathNonLispExtension {
4610 slot,
4611 path: path.to_path_buf(),
4612 });
4613 }
4614 }
4615 CodePathFileType::ComputeUnitYaml => {
4616 if !is_computeunit_yaml_extension(path) {
4617 return Err(ManifestError::CodePathNonComputeUnitYamlExtension {
4618 slot,
4619 path: path.to_path_buf(),
4620 });
4621 }
4622 }
4623 }
4624 crate::render::insert_first_seen(&mut seen, entry.as_str(), || {
4625 ManifestError::CodePathDuplicate {
4626 slot,
4627 path: path.to_path_buf(),
4628 }
4629 })?;
4630 }
4631 }
4632 Ok(())
4633 }
4634
4635 /// Reject `:etiquetas` lists with an empty entry or with two entries
4636 /// agreeing on the same string. `:etiquetas` is the universal
4637 /// registry-search-tag axis on [`Caixa`] (every kind carries the
4638 /// `Vec<String>` slot) and lands verbatim as the Helm chart
4639 /// `Chart.yaml` `keywords:` array on every Servico (caixa-helm's
4640 /// `build_chart_yaml` at `caixa-helm/src/lib.rs:236` folds it through
4641 /// a [`std::collections::BTreeSet`] alongside the four substrate-
4642 /// fixed tags `lareira` / `wasm` / `tatara-lisp` / `caixa-servico`).
4643 /// Two authoring footguns silently passed validate without this gate:
4644 ///
4645 /// - Empty entry (`(:etiquetas (""))` — the canonical paste-from-
4646 /// blank-doc footgun) rendered as `keywords: ["", "caixa-servico",
4647 /// "lareira", "tatara-lisp", "wasm"]` in `Chart.yaml`. Helm's
4648 /// `chart.metadata.keywords` admits the value without a strict
4649 /// parser-side gate, but the empty keyword has no operational
4650 /// meaning — it indexes nothing in the future caixa-registry
4651 /// search axis and clutters the rendered chart with a no-op tag.
4652 /// - Duplicate entries (`(:etiquetas ("demo" "demo"))` — the
4653 /// copy-paste-the-wrong-tag footgun) silently passed validate
4654 /// and were silently dedup'd by caixa-helm's `BTreeSet` collect
4655 /// at chart render — a "second wins / one silently disappears"
4656 /// shape divergent from every peer typed-graph set gate
4657 /// ([`crate::AplicacaoError::MembroDuplicate`] on `:membros`,
4658 /// [`crate::AplicacaoError::PlacementClusterDuplicate`] on
4659 /// `:placement :clusters`, [`crate::AplicacaoError::EntradaPathDuplicate`]
4660 /// on `:entrada :paths`, [`crate::AplicacaoError::ContratoDuplicate`]
4661 /// on `:contratos`, [`crate::DepError::DuplicateNome`] on
4662 /// `:deps` / `:deps-dev` per 359fba5, [`crate::UpgradeError::DuplicateFrom`]
4663 /// on `:upgrade-from`, the per-instruction-class singularity
4664 /// gates [`crate::UpgradeError::DuplicateLoadModule`] /
4665 /// [`crate::UpgradeError::DuplicateStateChange`] /
4666 /// [`crate::UpgradeError::DuplicateCleanup`]). The typed-graph
4667 /// discipline is uniform: every Vec-shaped author-supplied list
4668 /// past validate is set-not-multiset, by construction.
4669 ///
4670 /// Past the empty arm the gate enforces the chart-keyword shape
4671 /// predicate via [`crate::render::is_chart_keyword_shape`]: Cargo's
4672 /// crates.io `[package] keywords` grammar — 1..=20 bytes, starts
4673 /// with an ASCII letter, ASCII alphanumeric / `_` / `-`
4674 /// continuation. Closes the canonical paste-from-doc footguns the
4675 /// bare empty + duplicate arms left open: paste-from-aligned-doc
4676 /// whitespace (`" mesh"`, `"mesh "`), paste-from-multiline-doc
4677 /// newline (`"mesh\nhttp"` — the author pasted a multi-tag block
4678 /// into one entry instead of splitting), paste-from-Windows-CRLF-doc
4679 /// carriage return, CSV-list-separator confusion (`"mesh,http,grpc"`
4680 /// — the author meant three separate list entries), path-separator
4681 /// confusion (`"caixa/servico"`), namespace-suffix (`"http.1"`),
4682 /// leading-digit (`"1foo"`), kebab-leak (`"-foo"`), snake-leak
4683 /// (`"_foo"`), non-ASCII (`"café"`), and paste-from-binary-blob
4684 /// control bytes that would silently land as malformed search tags
4685 /// in the rendered Chart.yaml `keywords:` array and break the
4686 /// Artifact Hub keyword index lookup far from the source caixa.lisp.
4687 /// Mirrors the [`Self::validate_autores`] shape-predicate cascade
4688 /// established on the sibling universal-axis `Vec<String>` surface
4689 /// — the second universal-axis Vec<String> surface to land the
4690 /// empty-first-then-shape-then-duplicate per-entry cascade.
4691 ///
4692 /// Same empty-first cascade discipline every peer per-axis gate
4693 /// uses: the per-entry empty arm fires before the per-entry shape
4694 /// arm fires before the cross-entry duplicate arm, so an
4695 /// `("" "mesh" "mesh")` authoring shape surfaces the narrower
4696 /// [`ManifestError::EtiquetaEmpty`] (the structural "this entry
4697 /// has no value" defect) before either the shape or the duplicate
4698 /// diagnostic. Walks the list in declaration order so the
4699 /// first-collision diagnostic surfaces the lexicographically-
4700 /// earliest offending position, peer with every other duplicate
4701 /// gate on this surface.
4702 ///
4703 /// Universal-axis (every kind carries `:etiquetas`), so wired at the
4704 /// caixa-build gate alongside the peer universal gates
4705 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4706 /// [`Self::validate_deps`] / [`Self::validate_code_paths`] — before
4707 /// the kind-coherence gates ([`crate::LayoutError::MeshSlotsOnNonAplicacao`]
4708 /// / [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4709 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4710 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-specific
4711 /// slot sets. The future caixa-registry search axis can reach for
4712 /// `caixa.etiquetas` knowing every entry is a non-empty distinct
4713 /// chart-keyword-shaped string without re-deriving the precondition.
4714 pub fn validate_etiquetas(&self) -> Result<(), ManifestError> {
4715 let mut seen = std::collections::HashSet::new();
4716 for etiqueta in self.etiquetas() {
4717 if etiqueta.is_empty() {
4718 return Err(ManifestError::EtiquetaEmpty);
4719 }
4720 crate::render::is_chart_keyword_shape(etiqueta).map_err(|reason| {
4721 ManifestError::EtiquetaInvalid {
4722 etiqueta: etiqueta.clone(),
4723 reason,
4724 }
4725 })?;
4726 crate::render::insert_first_seen(&mut seen, etiqueta.as_str(), || {
4727 ManifestError::EtiquetaDuplicate {
4728 etiqueta: etiqueta.clone(),
4729 }
4730 })?;
4731 }
4732 Ok(())
4733 }
4734
4735 /// Reject `:autores` lists with an empty entry or with two entries
4736 /// agreeing on the same string. `:autores` is the universal
4737 /// maintainer-axis on [`Caixa`] (every kind carries the
4738 /// `Vec<String>` slot) and lands verbatim as the Helm chart
4739 /// `Chart.yaml` `maintainers:` array on every Servico (caixa-helm's
4740 /// `build_chart_yaml` at `caixa-helm/src/lib.rs:251` maps each entry
4741 /// to a `Maintainer { name, email: None }` without dedup). Two
4742 /// authoring footguns silently passed validate without this gate:
4743 ///
4744 /// - Empty entry (`(:autores (""))` — the canonical paste-from-
4745 /// blank-doc footgun) rendered as
4746 /// `maintainers: [{name: "", email: null}]` in `Chart.yaml`. The
4747 /// empty maintainer name has no operational meaning — it
4748 /// identifies no one in the substrate's authorship index and
4749 /// clutters the rendered chart with a no-op maintainer.
4750 /// - Duplicate entries (`(:autores ("pleme-io" "pleme-io"))` —
4751 /// the copy-paste-the-wrong-author footgun) silently passed
4752 /// validate and rendered as two identical maintainer entries.
4753 /// Unlike the [`Self::validate_etiquetas`] peer (caixa-helm's
4754 /// `BTreeSet`-collect on `:etiquetas` silently dedups the
4755 /// rendered `keywords:` array at chart-render time), the
4756 /// `maintainers:` rendering has *no* dedup — duplicate `:autores`
4757 /// entries stack verbatim in the chart, divergent from every
4758 /// peer typed-graph set gate ([`crate::AplicacaoError::MembroDuplicate`]
4759 /// on `:membros`, [`crate::AplicacaoError::PlacementClusterDuplicate`]
4760 /// on `:placement :clusters`, [`crate::AplicacaoError::EntradaPathDuplicate`]
4761 /// on `:entrada :paths`, [`crate::AplicacaoError::ContratoDuplicate`]
4762 /// on `:contratos`, [`crate::DepError::DuplicateNome`] on
4763 /// `:deps` / `:deps-dev`, [`crate::UpgradeError::DuplicateFrom`]
4764 /// on `:upgrade-from`, [`ManifestError::EtiquetaDuplicate`] on
4765 /// `:etiquetas`).
4766 ///
4767 /// Past the empty arm the gate enforces the chart-maintainer-name
4768 /// shape predicate via [`crate::render::is_chart_maintainer_name_shape`]:
4769 /// the structural single-line printable-UTF-8 floor every realistic
4770 /// Helm chart maintainer name carries — 1..=128 bytes, no leading
4771 /// or trailing whitespace, no ASCII control characters anywhere,
4772 /// Unicode bytes accepted. Closes the canonical paste-from-doc
4773 /// footguns the bare empty + duplicate arms left open:
4774 /// paste-from-aligned-doc whitespace (`" pleme-io"`, `"pleme-io "`),
4775 /// paste-from-multiline-doc newline (`"alice\nbob"` — the author
4776 /// pasted a multi-line block of author records into one `:autores`
4777 /// entry instead of splitting into one entry per author),
4778 /// paste-from-Windows-CRLF-doc carriage return, tab-from-aligned-doc,
4779 /// and the paste-from-binary-blob control bytes that would silently
4780 /// land as YAML-illegal byte sequences in the rendered Chart.yaml
4781 /// `maintainers:` array. Mirrors the shape-predicate cascade
4782 /// [`Self::validate_descricao`] / [`Self::validate_licenca`] /
4783 /// [`Self::validate_edicao`] / [`Self::validate_repositorio`]
4784 /// establish past their own empty arms on the sibling universal-axis
4785 /// `Option<String>` surfaces — the first universal-axis Vec<String>
4786 /// surface to land the empty-first-then-shape-then-duplicate per-entry
4787 /// cascade.
4788 ///
4789 /// Same empty-first cascade discipline every peer per-axis gate
4790 /// uses: the per-entry empty arm fires before the per-entry shape
4791 /// arm before the cross-entry duplicate arm. Walks the list in
4792 /// declaration order so the first-collision diagnostic surfaces the
4793 /// lexicographically-earliest offending position, peer with every
4794 /// other duplicate gate on this surface.
4795 ///
4796 /// Universal-axis (every kind carries `:autores`), so wired at the
4797 /// caixa-build gate alongside the peer universal gates
4798 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4799 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
4800 /// [`Self::validate_code_paths`] — before the kind-coherence gates
4801 /// ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4802 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4803 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4804 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-specific
4805 /// slot sets.
4806 pub fn validate_autores(&self) -> Result<(), ManifestError> {
4807 let mut seen = std::collections::HashSet::new();
4808 for autor in self.autores() {
4809 if autor.is_empty() {
4810 return Err(ManifestError::AutorEmpty);
4811 }
4812 crate::render::is_chart_maintainer_name_shape(autor).map_err(|reason| {
4813 ManifestError::AutorInvalid {
4814 autor: autor.clone(),
4815 reason,
4816 }
4817 })?;
4818 crate::render::insert_first_seen(&mut seen, autor.as_str(), || {
4819 ManifestError::AutorDuplicate {
4820 autor: autor.clone(),
4821 }
4822 })?;
4823 }
4824 Ok(())
4825 }
4826
4827 /// Reject `:repositorio` values whose shape the shared
4828 /// [`crate::render::is_git_repo_url`] predicate refuses. The flat
4829 /// `repositorio: Option<String>` slot on [`Caixa`] is the
4830 /// universal git-shaped homepage axis every kind carries — the
4831 /// substrate routes the same string through two load-bearing
4832 /// consumers:
4833 ///
4834 /// - [`caixa-helm`] folds it verbatim into the rendered
4835 /// `lareira-<nome>` Helm chart's `Chart.yaml` `home:` field
4836 /// (`build_chart_yaml` at `caixa-helm/src/lib.rs:268`) and into
4837 /// the chart `README.md` `repo = …` interpolation
4838 /// (`caixa-helm/src/lib.rs:359`).
4839 /// - [`caixa-flux`] folds it verbatim into the standalone
4840 /// `ClusterBundleOpts::for_caixa` `git_url:` field
4841 /// (`caixa-flux/src/lib.rs:293`), which becomes the `FluxCD`
4842 /// `GitRepository.spec.url` the cluster's source-controller
4843 /// polls — the load-bearing deploy-time axis.
4844 ///
4845 /// Both consumers use `Option::unwrap_or_else(|| <fallback>)` to
4846 /// substitute a placeholder when the slot is absent (`None` → the
4847 /// fallback fires); a `Some("")` *skips the fallback* and silently
4848 /// passes the empty string through to `Chart.yaml home: ""` /
4849 /// `GitRepository url: ""` — Helm's chart lint and `FluxCD`'s source
4850 /// controller both reject the empty URL far from the source
4851 /// `caixa.lisp`, with no field naming the offending `:repositorio`.
4852 /// Similarly a malformed `:repositorio` (whitespace, control char,
4853 /// missing `:` separator, leading `-`) silently lands in the
4854 /// rendered artifacts and breaks at `git clone` / `helm template`
4855 /// / `flux reconcile` time.
4856 ///
4857 /// Thin wrapper around [`crate::render::is_git_repo_url`] — the
4858 /// same shared predicate the peer [`crate::DepSource::validate`]
4859 /// routes the `:fonte (:tipo git :repo …)` axis through. With this
4860 /// gate the two `git URL`-shaped surfaces on the typed Caixa
4861 /// (`:repositorio` here, `:deps :fonte :repo` peer) are
4862 /// structurally equivalent: every value past validate is
4863 /// guaranteed-acceptable by the predicate's union of constraints
4864 /// (non-empty, length-bounded, no leading `-`, no whitespace, no
4865 /// control chars, ASCII only, no leading `:`, contains a `:`
4866 /// separator). The predicate accepts every documented authoring
4867 /// shape — `github:org/repo` shorthand, `https://host/path`,
4868 /// `ssh://[user@]host/path`, `git://host/path`, `git@host:path`
4869 /// scp-style SSH, `file:///path` — and refuses the canonical
4870 /// paste-from-blank-doc / paste-from-multiline-doc / CLI-arg-
4871 /// injection footguns at validate time. Maps the predicate's
4872 /// `String` reason verbatim into the
4873 /// [`ManifestError::RepositorioInvalid`] variant, carrying the
4874 /// offending value + parser-shaped reason so the diagnostic is
4875 /// self-locating (the author can grep their `caixa.lisp` for
4876 /// `:repositorio "<value>"` and fix it in one edit).
4877 ///
4878 /// `None` (the canonical "omit the slot to express no published
4879 /// homepage" shape) is accepted trivially — the gate is a no-op
4880 /// when the author didn't declare a value. `Some("")` is gated by
4881 /// the narrower [`ManifestError::RepositorioEmpty`] arm before the
4882 /// shape predicate is consulted, mirroring the empty-first cascade
4883 /// every peer per-axis identity gate uses
4884 /// ([`ManifestError::NomeEmpty`] → [`ManifestError::NomeInvalid`],
4885 /// [`ManifestError::VersaoEmpty`] → [`ManifestError::VersaoInvalid`],
4886 /// [`crate::DepError::FonteRepoEmpty`] →
4887 /// [`crate::DepError::FonteRepoInvalid`]).
4888 ///
4889 /// Universal-axis (every kind carries `:repositorio`), so wired at
4890 /// the caixa-build gate alongside the peer universal gates
4891 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4892 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
4893 /// [`Self::validate_autores`] / [`Self::validate_code_paths`] —
4894 /// before the kind-coherence gates
4895 /// ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4896 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4897 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4898 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-
4899 /// specific slot sets.
4900 pub fn validate_repositorio(&self) -> Result<(), ManifestError> {
4901 let Some(s) = self.repositorio() else {
4902 return Ok(());
4903 };
4904 if s.is_empty() {
4905 return Err(ManifestError::RepositorioEmpty);
4906 }
4907 is_git_repo_url(s).map_err(|reason| ManifestError::RepositorioInvalid {
4908 repositorio: s.to_string(),
4909 reason,
4910 })
4911 }
4912
4913 /// Reject `:descricao` values that are the empty string. The flat
4914 /// `descricao: Option<String>` slot on [`Caixa`] is the universal
4915 /// free-form-prose homepage axis every kind carries — the
4916 /// substrate routes the same string through two load-bearing
4917 /// consumers in the [`caixa-helm`] renderer:
4918 ///
4919 /// - `build_chart_yaml` folds it verbatim into the rendered
4920 /// `lareira-<nome>` Helm chart's `Chart.yaml` `description:`
4921 /// field (`caixa-helm/src/lib.rs:232-235`).
4922 /// - `build_readme` folds it verbatim into the rendered chart
4923 /// `README.md` header (`caixa-helm/src/lib.rs:333-336`).
4924 ///
4925 /// Both consumers use `Option::unwrap_or_else(|| <fallback>)` to
4926 /// substitute a `caixa.nome`-derived placeholder when the slot is
4927 /// absent (`None` → the fallback fires); a `Some("")` *skips the
4928 /// fallback* and silently passes the empty string through to
4929 /// `Chart.yaml description: ""` / a blank chart `README.md`
4930 /// header. Helm's chart spec requires a non-empty `description:`
4931 /// field on `apiVersion: v2` charts (`helm lint` surfaces it as
4932 /// `WARNING [chart.metadata.description]: description is required`),
4933 /// so the empty `Some("")` silently lands in the rendered
4934 /// artifacts and breaks at `helm lint` / `helm install` time far
4935 /// from the source `caixa.lisp`, with no field naming the
4936 /// offending `:descricao`.
4937 ///
4938 /// `None` (the canonical "omit the slot to defer to the renderer's
4939 /// `caixa.nome`-derived fallback" shape) is accepted trivially —
4940 /// the gate is a no-op when the author didn't declare a value.
4941 /// `Some("")` is gated by the narrower
4942 /// [`ManifestError::DescricaoEmpty`] arm, mirroring the empty-arm
4943 /// shape every peer per-axis empty gate uses
4944 /// ([`ManifestError::NomeEmpty`], [`ManifestError::VersaoEmpty`],
4945 /// [`ManifestError::EtiquetaEmpty`], [`ManifestError::AutorEmpty`],
4946 /// [`ManifestError::RepositorioEmpty`]).
4947 ///
4948 /// Universal-axis (every kind carries `:descricao`), so wired at
4949 /// the caixa-build gate alongside the peer universal gates
4950 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4951 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
4952 /// [`Self::validate_autores`] / [`Self::validate_repositorio`] /
4953 /// [`Self::validate_code_paths`] — before the kind-coherence
4954 /// gates ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4955 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4956 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4957 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-
4958 /// specific slot sets.
4959 ///
4960 /// Past the empty arm the gate enforces the chart-description
4961 /// shape predicate via [`crate::render::is_chart_description_shape`]:
4962 /// the structural single-line UTF-8 floor every realistic chart
4963 /// description in the wild matches — 1..=512 bytes, no leading
4964 /// or trailing whitespace, no ASCII control characters anywhere
4965 /// (`0x00..=0x1F` plus `0x7F` DEL — banning tab, newline,
4966 /// carriage return, and every other control byte), Unicode
4967 /// continuation bytes accepted (the canonical fixtures carry
4968 /// `→` and `—`). Closes the canonical paste-from-doc footguns
4969 /// the bare empty-arm gate left open: paste-from-aligned-doc
4970 /// leading / trailing whitespace (`" Checkout flow."`,
4971 /// `"Checkout flow. "`), paste-from-multiline-doc newline
4972 /// (`"Checkout\nflow."`), paste-from-Windows-CRLF-doc CR
4973 /// (`"Checkout\rflow."`), tab-from-aligned-doc
4974 /// (`"Checkout\tflow."`), and paste-from-binary-blob NUL / BEL /
4975 /// ESC / DEL bytes. Mirrors the shape-predicate cascade
4976 /// [`Self::validate_repositorio`] / [`Self::validate_licenca`] /
4977 /// [`Self::validate_edicao`] establish past their own empty arms
4978 /// on the sibling universal-axis `Option<String>` Caixa-level
4979 /// value-shape surfaces.
4980 ///
4981 /// The empty-first cascade discipline mirrors every peer per-axis
4982 /// identity gate: [`ManifestError::DescricaoEmpty`] runs before
4983 /// [`ManifestError::DescricaoInvalid`], so the narrower empty
4984 /// diagnostic surfaces on `Some("")` rather than the broader
4985 /// shape-predicate diagnostic — peer with how
4986 /// [`ManifestError::LicencaEmpty`] runs before
4987 /// [`ManifestError::LicencaInvalid`],
4988 /// [`ManifestError::EdicaoEmpty`] runs before
4989 /// [`ManifestError::EdicaoInvalid`],
4990 /// [`ManifestError::RepositorioEmpty`] runs before
4991 /// [`ManifestError::RepositorioInvalid`].
4992 pub fn validate_descricao(&self) -> Result<(), ManifestError> {
4993 let Some(s) = self.descricao() else {
4994 return Ok(());
4995 };
4996 if s.is_empty() {
4997 return Err(ManifestError::DescricaoEmpty);
4998 }
4999 crate::render::is_chart_description_shape(s).map_err(|reason| {
5000 ManifestError::DescricaoInvalid {
5001 descricao: s.to_string(),
5002 reason,
5003 }
5004 })?;
5005 Ok(())
5006 }
5007
5008 /// Reject `:licenca` values that are the empty string. The flat
5009 /// `licenca: Option<String>` slot on [`Caixa`] is the universal
5010 /// SPDX-shaped license-expression axis every kind carries — the
5011 /// substrate routes the same string through the [`caixa-helm`]
5012 /// renderer's `build_readme` which folds it verbatim into the
5013 /// rendered `lareira-<nome>` Helm chart's `README.md` `## License`
5014 /// section (`caixa-helm/src/lib.rs:361`) via
5015 /// `caixa.licenca.clone().unwrap_or_else(|| "MIT".into())`. The
5016 /// fallback only fires on `None`; a `Some("")` *skips the
5017 /// fallback* and silently passes the empty string through to a
5018 /// chart `README.md` whose `License` section renders as the bare
5019 /// trailing period (`.\n`) — peer footgun with the
5020 /// `Some("")`-skips-`unwrap_or_else` shape the
5021 /// [`Self::validate_descricao`] and [`Self::validate_repositorio`]
5022 /// gates close on the sibling free-form-prose and git-URL axes.
5023 ///
5024 /// `None` (the canonical "omit the slot to defer to the
5025 /// renderer's `MIT` fallback" shape every existing fixture
5026 /// carries) is accepted trivially — the gate is a no-op when the
5027 /// author didn't declare a value. `Some("")` is gated by the
5028 /// narrower [`ManifestError::LicencaEmpty`] arm, mirroring the
5029 /// empty-arm shape every peer per-axis empty gate uses
5030 /// ([`ManifestError::NomeEmpty`], [`ManifestError::VersaoEmpty`],
5031 /// [`ManifestError::EtiquetaEmpty`], [`ManifestError::AutorEmpty`],
5032 /// [`ManifestError::RepositorioEmpty`],
5033 /// [`ManifestError::DescricaoEmpty`]).
5034 ///
5035 /// Universal-axis (every kind carries `:licenca`), so wired at
5036 /// the caixa-build gate alongside the peer universal gates
5037 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
5038 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
5039 /// [`Self::validate_autores`] / [`Self::validate_repositorio`] /
5040 /// [`Self::validate_descricao`] / [`Self::validate_code_paths`]
5041 /// — before the kind-coherence gates
5042 /// ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
5043 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
5044 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
5045 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-
5046 /// specific slot sets.
5047 ///
5048 /// Past the empty arm the gate enforces the SPDX-expression shape
5049 /// predicate via [`crate::render::is_spdx_expression_shape`]: the
5050 /// structural alphabet floor every realistic SPDX expression in
5051 /// the wild uses — ASCII alphanumeric plus `.`, `-`, `+`, `(`,
5052 /// `)`, `:` (the `DocumentRef-…:LicenseRef-…` separator), and a
5053 /// single ASCII space (token separator). Closes the canonical
5054 /// paste-from-doc footguns the bare empty-arm gate left open:
5055 /// paste-from-doc whitespace (`"MIT "`, `" MIT"`), paste-from-
5056 /// multiline-doc CRLF (`"MIT\n"`), tab-from-aligned-doc
5057 /// (`"MIT\tOR Apache-2.0"`), non-ASCII smart-quote paste,
5058 /// underscore-instead-of-hyphen typo (`"Apache_2.0"`),
5059 /// comma-instead-of-`OR`-keyword colloquial idiom (`"MIT,
5060 /// Apache-2.0"`), slash-dual-license colloquial idiom (`"MIT/
5061 /// Apache-2.0"`), and semicolon-list-separator confusion
5062 /// (`"MIT; Apache-2.0"`). Mirrors the shape-predicate cascade
5063 /// [`Self::validate_repositorio`] / [`Self::validate_edicao`]
5064 /// establish past their own empty arms.
5065 ///
5066 /// The empty-first cascade discipline mirrors every peer per-axis
5067 /// identity gate: [`ManifestError::LicencaEmpty`] runs before
5068 /// [`ManifestError::LicencaInvalid`], so the narrower empty
5069 /// diagnostic surfaces on `Some("")` rather than the broader
5070 /// shape-predicate diagnostic — peer with how
5071 /// [`ManifestError::EdicaoEmpty`] runs before
5072 /// [`ManifestError::EdicaoInvalid`],
5073 /// [`ManifestError::RepositorioEmpty`] runs before
5074 /// [`ManifestError::RepositorioInvalid`].
5075 ///
5076 /// A future tightening on this axis can extend the alphabet
5077 /// floor into a full SPDX expression parser + license-id
5078 /// allowlist (rejecting alphabet-valid values that don't name a
5079 /// real SPDX license identifier — e.g., `"NotAReal"` is
5080 /// alphabet-valid but no `NotAReal` license-id exists). That
5081 /// parser only becomes meaningful past a real SPDX-spec
5082 /// dependency; this gate establishes the structural floor by
5083 /// refusing every non-SPDX-alphabet value at validate time.
5084 pub fn validate_licenca(&self) -> Result<(), ManifestError> {
5085 let Some(s) = self.licenca() else {
5086 return Ok(());
5087 };
5088 if s.is_empty() {
5089 return Err(ManifestError::LicencaEmpty);
5090 }
5091 crate::render::is_spdx_expression_shape(s).map_err(|reason| {
5092 ManifestError::LicencaInvalid {
5093 licenca: s.to_string(),
5094 reason,
5095 }
5096 })?;
5097 Ok(())
5098 }
5099
5100 /// Reject `:edicao` values that are the empty string. The flat
5101 /// `edicao: Option<String>` slot on [`Caixa`] is the universal
5102 /// language-edition axis every kind carries — it determines the
5103 /// tatara-lisp macro surface + compatibility flags the substrate
5104 /// applies when building a caixa, and lands verbatim in the
5105 /// `Caixa::template` author-time scaffold (the canonical
5106 /// `:edicao "2026"` line every `feira init` emits via
5107 /// [`Caixa::template`] at `caixa-core/src/manifest.rs:1193`) and
5108 /// in every renderer-side fixture (`caixa-helm/src/lib.rs:375`,
5109 /// `caixa-flux/src/lib.rs:445`, `caixa-mesh/src/lib.rs:629`,
5110 /// `caixa-core/src/render.rs:2510`) via
5111 /// `edicao: Some("2026".into())`.
5112 ///
5113 /// `None` (the canonical "omit the slot to defer to the
5114 /// substrate's default edition" shape every existing
5115 /// [`caixa-resolver`] integration test fixture carries via
5116 /// `edicao: None` — see `caixa-resolver/tests/git_integration.rs`)
5117 /// is accepted trivially — the gate is a no-op when the author
5118 /// didn't declare a value. `Some("")` is gated by the narrower
5119 /// [`ManifestError::EdicaoEmpty`] arm, mirroring the empty-arm
5120 /// shape every peer per-axis empty gate uses
5121 /// ([`ManifestError::NomeEmpty`], [`ManifestError::VersaoEmpty`],
5122 /// [`ManifestError::EtiquetaEmpty`], [`ManifestError::AutorEmpty`],
5123 /// [`ManifestError::RepositorioEmpty`],
5124 /// [`ManifestError::DescricaoEmpty`], [`ManifestError::LicencaEmpty`]).
5125 ///
5126 /// Universal-axis (every kind carries `:edicao`), so wired at
5127 /// the caixa-build gate alongside the peer universal gates
5128 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
5129 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
5130 /// [`Self::validate_autores`] / [`Self::validate_repositorio`] /
5131 /// [`Self::validate_descricao`] / [`Self::validate_licenca`] /
5132 /// [`Self::validate_code_paths`] — before the kind-coherence
5133 /// gates ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
5134 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
5135 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
5136 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-
5137 /// specific slot sets.
5138 ///
5139 /// Past the empty arm the gate enforces the canonical year-shape
5140 /// predicate: every documented tatara-lisp edition is a 4-digit
5141 /// ASCII decimal year (`"2026"` is the only edition currently
5142 /// minted; future-introduced siblings will follow the same
5143 /// shape, peer with Cargo's `[package] edition` grammar which
5144 /// every value Cargo has ever accepted matches — `"2015"`,
5145 /// `"2018"`, `"2021"`, `"2024"`). Any value that's not exactly
5146 /// 4 ASCII decimal bytes is rejected with the narrower
5147 /// [`ManifestError::EdicaoInvalid`] arm, mirroring the
5148 /// shape-predicate cascade [`Self::validate_repositorio`]
5149 /// establishes past its own empty arm
5150 /// ([`ManifestError::RepositorioEmpty`] →
5151 /// [`ManifestError::RepositorioInvalid`]). Closes the canonical
5152 /// paste-from-doc footguns the bare empty-arm gate left open:
5153 ///
5154 /// - leading / trailing whitespace from a paste-from-doc
5155 /// (`"2026 "`, `" 2026"`)
5156 /// - control characters / CRLF from a paste-from-multiline-doc
5157 /// (`"2026\n"`)
5158 /// - non-ASCII look-alikes from a fullwidth keyboard
5159 /// (`"2026"`) which would silently land as a non-ASCII
5160 /// string in the rendered caixa.lisp
5161 /// - free-form non-year values (`"x"`, `"latest"`,
5162 /// `"nightly"`) that have no operational meaning on the
5163 /// substrate's build-time edition selector
5164 /// - leading non-digit prefixes (`"v2026"`, `"e2026"`,
5165 /// `"r2026"`) — common version-tag idioms that don't apply
5166 /// to the year-shaped edition axis
5167 /// - decimal-shaped values (`"2026.1"`, `"2026.0"`) — every
5168 /// edition is a year, not a fractional version
5169 /// - wrong-length numeric values (`"26"`, `"202"`, `"20260"`,
5170 /// `"00026"`) that don't name a year
5171 ///
5172 /// `None` (the canonical "omit the slot to defer to the
5173 /// substrate's default edition" shape every existing
5174 /// [`caixa-resolver`] integration test fixture carries via
5175 /// `edicao: None` — see `caixa-resolver/tests/git_integration.rs`)
5176 /// is accepted trivially — the gate is a no-op when the author
5177 /// didn't declare a value. The empty-first cascade discipline
5178 /// mirrors every peer per-axis identity gate:
5179 /// [`ManifestError::EdicaoEmpty`] runs before
5180 /// [`ManifestError::EdicaoInvalid`], so the narrower empty
5181 /// diagnostic surfaces on `Some("")` rather than the broader
5182 /// shape-predicate diagnostic — peer with how
5183 /// [`ManifestError::NomeEmpty`] runs before
5184 /// [`ManifestError::NomeInvalid`],
5185 /// [`ManifestError::VersaoEmpty`] runs before
5186 /// [`ManifestError::VersaoInvalid`],
5187 /// [`ManifestError::RepositorioEmpty`] runs before
5188 /// [`ManifestError::RepositorioInvalid`].
5189 ///
5190 /// A future tightening on this axis can extend the shape
5191 /// predicate into a known-edition allowlist (rejecting
5192 /// year-shaped values that don't name a tatara-lisp edition
5193 /// the substrate actually understands — e.g., `"1999"` is
5194 /// year-shaped but no `1999` edition exists). That allowlist
5195 /// only becomes meaningful past the introduction of a sibling
5196 /// edition to `"2026"`; this gate establishes the structural
5197 /// floor by refusing every non-year-shaped value at validate
5198 /// time.
5199 pub fn validate_edicao(&self) -> Result<(), ManifestError> {
5200 let Some(s) = self.edicao() else {
5201 return Ok(());
5202 };
5203 if s.is_empty() {
5204 return Err(ManifestError::EdicaoEmpty);
5205 }
5206 if s.len() != 4 || !s.bytes().all(|b| b.is_ascii_digit()) {
5207 return Err(ManifestError::EdicaoInvalid {
5208 edicao: s.to_string(),
5209 reason: "must be a 4-digit ASCII decimal year (canonical \"2026\")".to_string(),
5210 });
5211 }
5212 Ok(())
5213 }
5214
5215 /// Compose the supervisor-related flat slots into a single
5216 /// [`SupervisorSpec`] for validation. Returns `None` when the
5217 /// caixa isn't a `:kind Supervisor`.
5218 ///
5219 /// The flat representation in [`Caixa`] keeps tatara-lisp authoring
5220 /// simple (one form, no nested `:supervisor (…)` block); this view
5221 /// is the "typed shape" the operator + supervisor reconciler
5222 /// consume.
5223 #[must_use]
5224 pub fn supervisor_view(&self) -> Option<SupervisorSpec> {
5225 if !self.kind().is_supervisor() {
5226 return None;
5227 }
5228 // Fold through the shared `supervisor::duration_codec::parse`
5229 // — the same parser the serde-routed `with = "duration_codec"`
5230 // on `SupervisorSpec::restart_window`, the `:politicas
5231 // :timeout` codec, and the `:politicas :circuit-breaker
5232 // :window` codec all consume. The prior inline f64-shaped
5233 // duplicate (`parse_window_inline`) admitted every magnitude
5234 // the integer-magnitude gate (1c55a2a) rejects on the three
5235 // serde-routed siblings — `"1.5s"`, `"1.0s"`, `"0.5m"`,
5236 // `"+30s"`, `"-30s"` — and silently dropped malformed input as
5237 // `None` (i.e. "no reset"), divergent from the shared codec's
5238 // integer-magnitude discipline by construction. The fold
5239 // closes the divergence: every value the typed
5240 // `SupervisorSpec` carries past `supervisor_view` is in the
5241 // shared codec's accepted set. The `.ok()` here preserves the
5242 // existing soft-swallow shape on this view-construction path;
5243 // the new [`Caixa::validate_restart_window`] (sibling of
5244 // [`Self::validate_nome`] / [`Self::validate_versao`]) names
5245 // the offending raw string at build time so authoring tools
5246 // (`feira lint`, the future layout-side wire-up) surface a
5247 // self-locating diagnostic instead of a silently dropped
5248 // window.
5249 let restart_window = self
5250 .restart_window()
5251 .and_then(|s| crate::supervisor::duration_codec::parse(s).ok());
5252 Some(SupervisorSpec {
5253 estrategia: self.estrategia().unwrap_or_default(),
5254 // Route the author-omitted `:max-restarts` arm through the
5255 // substrate-canonical [`crate::supervisor::SUPERVISOR_MAX_RESTARTS_DEFAULT`]
5256 // typed `pub const` rather than the raw `5` literal — one
5257 // source of truth for the Erlang/OTP-canonical
5258 // `{intensity, 5, 60}` `MaxIntensity` default that also
5259 // backs the serde-side wire-format author-omitted arm on
5260 // [`crate::supervisor::SupervisorSpec::max_restarts`] via
5261 // `#[serde(default = "default_max_restarts")]` and the
5262 // [`Default for SupervisorSpec`] impl's struct-literal
5263 // default field. Prior to the lift the composition site
5264 // carried a raw `5` with no compile-time link back to the
5265 // serde-side default, so a future rebrand of the OTP-
5266 // canonical default (a tightening to Elixir's `3`, a
5267 // widening to a per-cluster overlay the operator pins
5268 // through the MESH-COMPOSITION §III.2 supervision-canary
5269 // `:supervisor :max-restarts-overrides` roadmap slot)
5270 // would have had to be threaded through both open-coded
5271 // copies in lockstep or the wire-format author-omitted arm
5272 // and this view-construction author-omitted arm would
5273 // silently disagree on which restart-budget an omitted
5274 // `:max-restarts` resolves to. Pinned by
5275 // [`supervisor_view_max_restarts_fallback_routes_through_lifted_default`]
5276 // in the tests module.
5277 max_restarts: self
5278 .max_restarts()
5279 .unwrap_or(crate::supervisor::SUPERVISOR_MAX_RESTARTS_DEFAULT),
5280 restart_window,
5281 children: self.children().to_vec(),
5282 })
5283 }
5284
5285 /// A minimal starter manifest emitted by `feira init`.
5286 #[must_use]
5287 pub fn template(nome: &str) -> String {
5288 format!(
5289 "(defcaixa\n \
5290 :nome {nome:?}\n \
5291 :versao \"0.1.0\"\n \
5292 :kind Biblioteca\n \
5293 :edicao \"2026\"\n \
5294 :descricao \"FIXME — describe this caixa\"\n \
5295 :autores ()\n \
5296 :etiquetas ()\n \
5297 :deps ()\n \
5298 :deps-dev ()\n \
5299 :bibliotecas (\"lib/{nome}.lisp\"))\n"
5300 )
5301 }
5302
5303 /// Serialize to a canonical `caixa.lisp` source — suitable for writing
5304 /// back after mutation (e.g. `feira add`).
5305 ///
5306 /// Goes through serde JSON → canonical Sexp → per-field pretty print.
5307 /// The derive-macro `compile_from_sexp` path is the inverse, so any
5308 /// `Caixa` round-trips through `to_lisp` + `from_lisp`.
5309 #[must_use]
5310 pub fn to_lisp(&self) -> String {
5311 let json = serde_json::to_value(self).expect("Caixa serialize");
5312 let sexp = tatara_lisp::domain::json_to_sexp(&json);
5313 let tatara_lisp::Sexp::List(items) = sexp else {
5314 return format!("(defcaixa {sexp})\n");
5315 };
5316 let mut out = String::from("(defcaixa");
5317 let mut i = 0;
5318 while i + 1 < items.len() {
5319 out.push_str("\n ");
5320 out.push_str(&items[i].to_string());
5321 out.push(' ');
5322 out.push_str(&items[i + 1].to_string());
5323 i += 2;
5324 }
5325 out.push_str(")\n");
5326 out
5327 }
5328}
5329
5330/// Errors raised by top-level [`Caixa`] validators that don't fit
5331/// the per-axis [`DepError`] / [`crate::AplicacaoError`] /
5332/// [`crate::SupervisorError`] / [`crate::LayoutError`] families —
5333/// the Caixa's own identity axes (`:nome`, `:versao`) that flow
5334/// through every substrate-side artifact's `metadata.name` /
5335/// version derivation.
5336///
5337/// A future top-level sum (the M4 `CaixaError` the [`DepError`]
5338/// doc-comment anticipates) can hold one of each per-axis error
5339/// family without reshaping individual diagnostics; this enum is
5340/// the first such per-Caixa-identity family.
5341#[derive(Debug, Error, PartialEq, Eq)]
5342pub enum ManifestError {
5343 #[error(
5344 ":nome is empty (every caixa must name itself; the value flows \
5345 into every K8s artifact's `metadata.name` derivation and into \
5346 the default `lib/<nome>.lisp` / `exe/<nome>` layout paths)"
5347 )]
5348 NomeEmpty,
5349 #[error(
5350 ":nome {nome:?} is not a valid DNS-1123 label: {reason} (the K8s \
5351 apiserver enforces this rule on every `metadata.name` the \
5352 caixa's substrate-side renderers derive from `:nome` — the \
5353 `lareira-<nome>` Helm chart name, the programs.yaml entry \
5354 name, the `LABEL_APLICACAO` label value, the `<aplicacao>-<de>-to-<para>` \
5355 CiliumNetworkPolicy name, the `<aplicacao>-<para>` HTTPRoute \
5356 name; use a lowercase alphanumeric + hyphen identifier like \
5357 `\"checkout\"` or `\"cart-v2\"`)"
5358 )]
5359 NomeInvalid { nome: String, reason: String },
5360 #[error(
5361 ":nome {nome:?} overflows the joint-length budget on the canonical \
5362 `lareira-<nome>` chart-name shape: {reason} (every per-Servico / \
5363 per-Aplicacao renderer the substrate carries — `caixa-helm`'s \
5364 `Chart.yaml::name`, `caixa-flux`'s `cluster_bundle` HelmRelease \
5365 `chart:` slot, `caixa-tatara`'s `release_name` + \
5366 `oci://<registry>/lareira-<nome>` chart ref — derives the same \
5367 joint name through the canonical `lareira_chart_name` helper, and \
5368 Helm's `Chart.yaml::name` admission rule + the K8s apiserver's \
5369 DNS-1123 label cap on every chart-name-derived `metadata.name` \
5370 reject any joint name exceeding 63 bytes; the narrower \
5371 `:nome` shape (`NomeInvalid`) gates the bare-`:nome` budget, this \
5372 arm gates the chart-name budget downstream renderers inherit)"
5373 )]
5374 NomeChartNameBudgetExceeded { nome: String, reason: String },
5375 #[error(
5376 ":versao is empty (every caixa must pin its own version; the value flows \
5377 into the `lareira-<nome>` Helm chart's `Chart.yaml` version + appVersion, \
5378 the `feira publish` `v<versao>` git tag, the OCI image's `:v<versao>` / \
5379 `:latest` tags, the lacre closure's `concrete_versao`, and the \
5380 `:upgrade-from :from` peers — use a SemVer-2 literal like `\"0.1.0\"`)"
5381 )]
5382 VersaoEmpty,
5383 #[error(
5384 ":versao {versao:?} is not a valid SemVer-2 version: {reason} (the substrate \
5385 consumes this string as `semver::Version` — three-part `MAJOR.MINOR.PATCH` \
5386 with optional `-prerelease` and `+build` — across every artifact derived \
5387 from `:versao`: the `lareira-<nome>` Helm chart's `Chart.yaml` version + \
5388 appVersion (Helm SemVer-2-strict), the `feira publish` `v<versao>` git tag, \
5389 the OCI image's `:v<versao>` tag, the lacre closure's `concrete_versao`, \
5390 and the `:upgrade-from :from` peers that match against this exact shape; \
5391 use a literal like `\"0.1.0\"`, `\"0.2.0-rc.1\"`, or `\"1.0.0+build.42\"` — \
5392 not a git-tag-shape like `\"v0.1.0\"`, a docker-tag-shape like `\"latest\"`, \
5393 a requirement-shape like `\"^0.1\"`, or a four-part `\"0.1.0.0\"`)"
5394 )]
5395 VersaoInvalid { versao: String, reason: String },
5396 #[error(
5397 ":restart-window {restart_window:?} is not a valid duration: {reason} (the \
5398 substrate consumes this string through the shared \
5399 `supervisor::duration_codec` — the same parser routed via `with = \
5400 \"duration_codec\"` onto the typed `SupervisorSpec::restart_window`, \
5401 `:politicas :timeout`, and `:politicas :circuit-breaker :window` slots; \
5402 the canonical authoring form is `<integer><unit>` where the unit is one \
5403 of `ms` / `s` / `m` / `h` and the magnitude has no decimal point and no \
5404 leading `+` / `-` sign — e.g. `\"60s\"`, `\"5m\"`, `\"1h\"`, `\"500ms\"`. \
5405 Without this gate a malformed `:restart-window` silently produced a \
5406 supervisor with `restart_window: None` (\"never reset\"), turning OTP's \
5407 `MaxIntensity / Period` invariant into a never-reset supervisor far from \
5408 the source `caixa.lisp`; the gate moves the diagnostic to the manifest \
5409 layer with the offending value named verbatim. Omit the slot entirely to \
5410 express \"no reset\"; carry a positive integer duration to express the \
5411 sliding window)"
5412 )]
5413 RestartWindowMalformed {
5414 restart_window: String,
5415 reason: String,
5416 },
5417 #[error(
5418 "{slot} entry is an empty path string — every {slot} entry must name \
5419 a file relative to the caixa root; omit the entry to omit the file \
5420 (the layout checker's `root.join(\"\")` resolves to the caixa root \
5421 itself, so an empty entry silently aliases the project root as a \
5422 declared {slot} file, then fails downstream at parse / existence \
5423 time with a diagnostic that names the root rather than the offending \
5424 entry)"
5425 )]
5426 CodePathEmpty { slot: &'static str },
5427 #[error(
5428 "{slot} entry {} is an absolute path — entries must be relative to \
5429 the caixa root, since `Path::join` replaces the base with an absolute \
5430 right-hand side and `root.join(\"/abs/...\")` resolves to \"/abs/...\" \
5431 outside the caixa root sandbox; rewrite the entry as a relative path \
5432 under the caixa root (e.g. `\"lib/<name>.lisp\"`, `\"exe/<name>\"`, \
5433 `\"servicos/<name>.computeunit.yaml\"`)",
5434 path.display()
5435 )]
5436 CodePathAbsolute { slot: &'static str, path: PathBuf },
5437 #[error(
5438 "{slot} entry {} contains a `..` component — entries must not traverse \
5439 above the caixa root (the layout's `starts_with(<dir>)` fence on \
5440 `:exe` / `:servicos` is component-aware, not canonical-path-aware, \
5441 so a mid-path `..` silently traverses the sandbox; `:bibliotecas` \
5442 has no such fence, so a leading `..` escapes unconditionally if the \
5443 resolved target happens to exist)",
5444 path.display()
5445 )]
5446 CodePathParentEscape { slot: &'static str, path: PathBuf },
5447 #[error(
5448 "{slot} entry {} does not terminate in the `.lisp` extension — every \
5449 `:bibliotecas` entry is a tatara-lisp source file the `feira build` \
5450 loop reads through `tatara_lisp::read` at parse time, so any other \
5451 extension (`.rs`, `.txt`, `.lisp.bak`) or no-extension shape is \
5452 structurally a parser error far from the source caixa.lisp, with \
5453 no field naming the offending `:bibliotecas` entry. Pin a relative \
5454 path under the caixa root whose terminating extension is \
5455 lowercase-`.lisp` (e.g. `\"lib/<name>.lisp\"`, \
5456 `\"lib/handlers.lisp\"`) — the same file-type contract the peer \
5457 `:behavior :on-*` (c97815a) and `:upgrade-from :state-change :script` \
5458 (33cc830) axes already carry through the same lifted \
5459 `is_lisp_extension` predicate",
5460 path.display()
5461 )]
5462 CodePathNonLispExtension { slot: &'static str, path: PathBuf },
5463 #[error(
5464 "{slot} entry {} does not terminate in the `.computeunit.yaml` \
5465 compound suffix — every `:servicos` entry is a typed `ComputeUnit` \
5466 CR YAML file the peer caixa-helm / caixa-flux renderers consume \
5467 through `serde_yaml::from_str` at chart / FluxCD bundle render \
5468 time, so any other extension (`.yaml`, `.yml`, `.json`, the \
5469 off-by-one-segment `.computeunit-yaml`, the editor-backup \
5470 `.computeunit.yaml.bak`) or no-extension shape is structurally a \
5471 YAML-parser error / `ComputeUnit` schema-mismatch far from the \
5472 source caixa.lisp, with no field naming the offending `:servicos` \
5473 entry. Pin a relative path under the caixa root whose terminating \
5474 compound suffix is lowercase-`.computeunit.yaml` (e.g. \
5475 `\"servicos/<name>.computeunit.yaml\"`, \
5476 `\"servicos/hello-rio.computeunit.yaml\"`) — the same file-type \
5477 contract the sibling `:bibliotecas` axis (64772a9) already carries \
5478 on the tatara-lisp-source axis through the peer lifted \
5479 `is_lisp_extension` predicate, here on the compound-suffix axis \
5480 `Path::extension` can't express on its own through the lifted \
5481 `is_computeunit_yaml_extension` predicate",
5482 path.display()
5483 )]
5484 CodePathNonComputeUnitYamlExtension { slot: &'static str, path: PathBuf },
5485 #[error(
5486 "{slot} entry {} appears more than once (the code-path list is \
5487 a set, not a multiset; every peer Vec-shaped author-supplied \
5488 list past validate is set-not-multiset — `:membros :caixa`, \
5489 `:placement :clusters`, `:entrada :paths`, `:contratos`, \
5490 `:children :caixa`, `:deps` / `:deps-dev` `:nome`, \
5491 `:upgrade-from :from`, `:etiquetas`, `:autores` — and the three \
5492 code-path lists are the last Vec-shaped author-supplied slots on \
5493 the typed Caixa surface still admitting a duplicate entry. \
5494 `:bibliotecas` duplicates re-parse the same file at \
5495 `feira build` time and silently mask the author's intent to \
5496 declare a *second* biblioteca; `:exe` duplicates collide on the \
5497 flake `packages.<name>` derivation key at the future \
5498 `caixa-flake` materializer; `:servicos` duplicates surface as the \
5499 narrower [`caixa-helm`] / [`caixa-flux`] `UnsupportedServicoCount` \
5500 rejection far from the source `caixa.lisp`. Drop the duplicate \
5501 or rename it to the actual second file intended)",
5502 path.display()
5503 )]
5504 CodePathDuplicate { slot: &'static str, path: PathBuf },
5505 #[error(
5506 ":etiquetas entry is empty (every tag must carry a non-empty \
5507 registry-search identifier; the empty entry has no operational \
5508 meaning — it indexes nothing in the future caixa-registry search \
5509 axis and clutters the rendered Helm `Chart.yaml` `keywords:` array \
5510 with a no-op tag; omit the entry to express \"no tag on this \
5511 position\")"
5512 )]
5513 EtiquetaEmpty,
5514 #[error(
5515 ":etiquetas entry {etiqueta:?} appears more than once (the \
5516 registry-search tag set is a set, not a multiset; duplicate \
5517 entries are silently dedup'd by caixa-helm's `BTreeSet` collect \
5518 at chart render — a \"second wins / one silently disappears\" \
5519 shape divergent from every peer typed-graph set gate \
5520 (`:membros :caixa`, `:placement :clusters`, `:entrada :paths`, \
5521 `:contratos`, `:deps :nome`, `:upgrade-from :from`); drop the \
5522 duplicate or rename it to the actual tag intended)"
5523 )]
5524 EtiquetaDuplicate { etiqueta: String },
5525 #[error(
5526 ":etiquetas entry {etiqueta:?} is not a valid chart-keyword shape: \
5527 {reason} (the substrate consumes this string through the shared \
5528 `crate::render::is_chart_keyword_shape` predicate — the same \
5529 Cargo crates.io `[package] keywords` grammar entry shape: 1..=20 \
5530 bytes, starts with an ASCII letter, ASCII alphanumeric / `_` / `-` \
5531 continuation. The canonical authoring shapes are short kebab-case \
5532 identifiers like `\"mesh\"`, `\"wasm\"`, `\"tatara-lisp\"`, \
5533 `\"hello-world\"`, `\"caixa-servico\"`, `\"infrastructure\"`. \
5534 Without this gate a malformed `:etiquetas` entry (paste-from-doc \
5535 leading / trailing whitespace `\" mesh\"` / `\"mesh \"`; \
5536 paste-from-multiline-doc newline `\"mesh\\nhttp\"`; \
5537 paste-from-Windows-CRLF-doc CR; CSV-list-separator confusion \
5538 `\"mesh,http,grpc\"` — the author meant to author three separate \
5539 list entries; path-separator confusion `\"caixa/servico\"`; \
5540 namespace-suffix `\"http.1\"`; leading-digit `\"1foo\"`; \
5541 kebab-leak `\"-foo\"`; snake-leak `\"_foo\"`; non-ASCII \
5542 `\"café\"` — every legitimate search tag is strict ASCII; \
5543 paste-from-binary-blob NUL / BEL / ESC / DEL byte) silently \
5544 passed `from_lisp` + `validate_etiquetas` + \
5545 `StandardLayout::verify` and landed in the rendered \
5546 `lareira-<nome>` Helm chart's `Chart.yaml keywords:` array as a \
5547 malformed search tag — Artifact Hub's keyword index + the future \
5548 caixa-registry's keyword index would either silently drop the \
5549 tag or fail to index it far from the source caixa.lisp; the gate \
5550 moves the diagnostic to the manifest layer with the offending \
5551 value named verbatim)"
5552 )]
5553 EtiquetaInvalid { etiqueta: String, reason: String },
5554 #[error(
5555 ":autores entry is empty (every maintainer must carry a non-empty \
5556 identifier; the empty entry has no operational meaning — it \
5557 identifies no one in the substrate's authorship index and renders \
5558 as `maintainers: [{{name: \"\", email: null}}]` in the Helm chart's \
5559 `Chart.yaml`, a no-op maintainer the substrate cannot route to; \
5560 omit the entry to express \"no maintainer on this position\")"
5561 )]
5562 AutorEmpty,
5563 #[error(
5564 ":autores entry {autor:?} appears more than once (the maintainer \
5565 set is a set, not a multiset; unlike `:etiquetas`, caixa-helm's \
5566 `maintainers:` rendering does *no* dedup — duplicate entries \
5567 stack verbatim in `Chart.yaml` as two identical \
5568 `Maintainer {{ name, email: None }}` records, divergent from every \
5569 peer typed-graph set gate (`:etiquetas`, `:membros :caixa`, \
5570 `:placement :clusters`, `:entrada :paths`, `:contratos`, \
5571 `:deps :nome`, `:upgrade-from :from`); drop the duplicate or \
5572 rename it to the actual author intended)"
5573 )]
5574 AutorDuplicate { autor: String },
5575 #[error(
5576 ":autores entry {autor:?} is not a valid chart-maintainer-name shape: \
5577 {reason} (the substrate consumes this string through the shared \
5578 `crate::render::is_chart_maintainer_name_shape` predicate — the same \
5579 single-line-UTF-8 floor every realistic chart maintainer name carries: \
5580 1..=128 bytes, no leading or trailing whitespace, no ASCII control \
5581 characters anywhere, Unicode bytes accepted. The canonical authoring \
5582 shapes are short single-line identifiers like `\"pleme-io\"`, \
5583 `\"Pleme Contributors\"`, `\"alice <alice@example.com>\"`, \
5584 `\"François Dupont\"`. Without this gate a malformed `:autores` entry \
5585 (paste-from-aligned-doc leading whitespace `\" pleme-io\"` / trailing \
5586 whitespace `\"pleme-io \"`; paste-from-multiline-doc newline \
5587 `\"alice\\nbob\"` — the author pasted a multi-line block of author \
5588 records into one entry instead of splitting into one entry per author; \
5589 paste-from-Windows-CRLF-doc carriage return `\"alice\\rbob\"`; \
5590 tab-from-aligned-doc `\"Pleme\\tContributors\"`; paste-from-binary-blob \
5591 NUL / BEL / ESC / DEL byte) silently passed `from_lisp` + \
5592 `validate_autores` + `StandardLayout::verify` and landed in the \
5593 rendered `lareira-<nome>` Helm chart's `Chart.yaml maintainers:` array \
5594 as a YAML-illegal multi-line scalar or a silently-trimmed whitespace \
5595 round-trip — every chart-aware UI (`helm list`, `helm search`, \
5596 Artifact Hub maintainer index) would render the maintainer name in a \
5597 single-line column far from the source caixa.lisp; the gate moves the \
5598 diagnostic to the manifest layer with the offending value named \
5599 verbatim)"
5600 )]
5601 AutorInvalid { autor: String, reason: String },
5602 #[error(
5603 ":repositorio is the empty string (every published caixa names its \
5604 git source via a non-empty `:repositorio` locator — the value \
5605 flows verbatim into the rendered `lareira-<nome>` Helm chart's \
5606 `Chart.yaml` `home:` field via `caixa-helm` and into the FluxCD \
5607 `GitRepository.spec.url` via `caixa-flux`'s \
5608 `ClusterBundleOpts::for_caixa`; both consumers' \
5609 `Option::unwrap_or_else` fallbacks only fire when the slot is \
5610 `None`, so an empty `Some(\"\")` silently lands as `home: \"\"` / \
5611 `url: \"\"` in the rendered artifacts and breaks at `helm \
5612 template` / FluxCD source-controller reconcile time far from the \
5613 source caixa.lisp; omit the slot entirely to defer to the \
5614 renderer's `https://github.com/pleme-io/<nome>` / \
5615 `caixa.nome`-derived fallback, or carry a canonical authoring \
5616 shape like `\"github:org/repo\"`, `\"https://host/path\"`, \
5617 `\"ssh://[user@]host/path\"`, `\"git@host:path\"`, or \
5618 `\"file:///path\"`)"
5619 )]
5620 RepositorioEmpty,
5621 #[error(
5622 ":repositorio {repositorio:?} is not a valid git repo URL: {reason} \
5623 (the substrate consumes this string through the shared \
5624 `crate::render::is_git_repo_url` predicate — the same parser the \
5625 peer `:deps :fonte (:tipo git :repo …)` axis routes its `:repo` \
5626 value through via `DepSource::validate`; the canonical authoring \
5627 shapes are `\"github:org/repo\"` shorthand, `\"https://host/path\"` \
5628 / `\"ssh://[user@]host/path\"` / `\"git://host/path\"` / \
5629 `\"file:///path\"` URL schemes, or the `\"git@host:path\"` \
5630 scp-style SSH form. Without this gate a malformed `:repositorio` \
5631 (whitespace from a paste-from-doc; control characters / CRLF \
5632 from a paste-from-multiline-doc; a leading `-` from a \
5633 CLI-argument-injection footgun; a missing `:` separator from a \
5634 bare `org/repo` shape git treats as a relative filesystem path) \
5635 silently landed in the rendered `Chart.yaml home:` and the \
5636 FluxCD `GitRepository.spec.url` and broke at `git clone` / \
5637 FluxCD reconcile time far from the source caixa.lisp; the gate \
5638 moves the diagnostic to the manifest layer with the offending \
5639 value named verbatim)"
5640 )]
5641 RepositorioInvalid { repositorio: String, reason: String },
5642 #[error(
5643 ":descricao is the empty string (every published caixa names \
5644 its purpose via a non-empty `:descricao` summary — the value \
5645 flows verbatim into the rendered `lareira-<nome>` Helm \
5646 chart's `Chart.yaml` `description:` field via `caixa-helm`'s \
5647 `build_chart_yaml` and into the chart `README.md` header via \
5648 `build_readme`; both consumers' `Option::unwrap_or_else` \
5649 `caixa.nome`-derived fallbacks only fire when the slot is \
5650 `None`, so an empty `Some(\"\")` silently lands as \
5651 `description: \"\"` / a blank `README.md` header in the \
5652 rendered artifacts and breaks at `helm lint` time \
5653 (`WARNING [chart.metadata.description]: description is \
5654 required` on `apiVersion: v2` charts) far from the source \
5655 caixa.lisp; omit the slot entirely to defer to the \
5656 renderer's `\"Generated chart for caixa Servico <nome>\"` / \
5657 `\"caixa Servico <nome>\"` fallbacks, or carry a non-empty \
5658 summary like `\"Canonical Rust→wasm32-wasip2 caixa \
5659 Servico.\"`)"
5660 )]
5661 DescricaoEmpty,
5662 #[error(
5663 ":descricao {descricao:?} is not a valid chart-description shape: \
5664 {reason} (the substrate consumes this string through the shared \
5665 `crate::render::is_chart_description_shape` predicate — the same \
5666 single-line-UTF-8 floor every realistic chart description carries: \
5667 1..=512 bytes, no leading or trailing whitespace, no ASCII control \
5668 characters anywhere, Unicode prose bytes accepted. The canonical \
5669 authoring shapes are short single-line summaries like `\"Canonical \
5670 Rust→wasm32-wasip2 caixa Servico.\"`, `\"Checkout flow.\"`, \
5671 `\"AWS provider caixa for tatara-lisp\"`. Without this gate a \
5672 malformed `:descricao` (paste-from-aligned-doc leading whitespace \
5673 `\" Checkout flow.\"` / trailing whitespace `\"Checkout flow. \"`; \
5674 paste-from-multiline-doc newline `\"Checkout\\nflow.\"`; \
5675 paste-from-Windows-CRLF-doc carriage return `\"Checkout\\rflow.\"`; \
5676 tab-from-aligned-doc `\"Checkout\\tflow.\"`; paste-from-binary-blob \
5677 NUL / BEL / ESC / DEL byte) silently passed `from_lisp` + \
5678 `validate_descricao` + `StandardLayout::verify` and landed in the \
5679 rendered `lareira-<nome>` Helm chart's `Chart.yaml description:` \
5680 field + `README.md` header paragraph as a YAML-illegal multi-line \
5681 scalar or a silently-trimmed whitespace round-trip — every \
5682 chart-aware UI (`helm list`, `helm search`, Artifact Hub) would \
5683 render the description in a single-line column far from the source \
5684 caixa.lisp; the gate moves the diagnostic to the manifest layer \
5685 with the offending value named verbatim)"
5686 )]
5687 DescricaoInvalid { descricao: String, reason: String },
5688 #[error(
5689 ":licenca is the empty string (every published caixa names \
5690 its license via a non-empty `:licenca` SPDX expression — the \
5691 value flows verbatim into the rendered `lareira-<nome>` Helm \
5692 chart's `README.md` `## License` section via `caixa-helm`'s \
5693 `build_readme` at `caixa-helm/src/lib.rs:361`; the consumer's \
5694 `Option::unwrap_or_else(|| \"MIT\".into())` `MIT` fallback \
5695 only fires when the slot is `None`, so an empty `Some(\"\")` \
5696 silently lands as a bare trailing period in the rendered \
5697 chart `README.md` `License` section far from the source \
5698 caixa.lisp; omit the slot entirely to defer to the \
5699 renderer's `MIT` fallback, or carry a canonical SPDX \
5700 expression like `\"MIT\"`, `\"Apache-2.0\"`, \
5701 `\"Apache-2.0 OR MIT\"`)"
5702 )]
5703 LicencaEmpty,
5704 #[error(
5705 ":licenca {licenca:?} is not a valid SPDX expression shape: {reason} \
5706 (the substrate consumes this string through the shared \
5707 `crate::render::is_spdx_expression_shape` predicate — the same \
5708 alphabet-floor parser every peer per-axis value-shape gate routes \
5709 its value through; the canonical authoring shapes are single \
5710 license identifiers like `\"MIT\"`, `\"Apache-2.0\"`, `\"BSD-3-Clause\"`, \
5711 compound expressions like `\"Apache-2.0 OR MIT\"`, \
5712 `\"MIT AND BSD-3-Clause\"`, `\"(MIT OR Apache-2.0) AND ISC\"`, \
5713 license-with-exception forms like `\"Apache-2.0 WITH LLVM-exception\"`, \
5714 `+`-suffix variants like `\"GPL-2.0+\"`, and user-defined references \
5715 like `\"LicenseRef-MyLicense\"` / \
5716 `\"DocumentRef-doc:LicenseRef-MyLicense\"`. Without this gate a \
5717 malformed `:licenca` (paste-from-doc whitespace `\"MIT \"` / \
5718 `\" MIT\"`; paste-from-multiline-doc CRLF `\"MIT\\n\"`; \
5719 tab-from-aligned-doc `\"MIT\\tOR Apache-2.0\"`; non-ASCII byte from \
5720 a smart-quote paste; underscore-instead-of-hyphen typo \
5721 `\"Apache_2.0\"`; comma-instead-of-`OR`-keyword colloquial idiom \
5722 `\"MIT, Apache-2.0\"`; slash-dual-license colloquial idiom \
5723 `\"MIT/Apache-2.0\"`; semicolon-list-separator confusion \
5724 `\"MIT; Apache-2.0\"`) silently landed in the rendered chart \
5725 `README.md` `## License` section + a future SPDX-aware \
5726 `Chart.yaml license:` emitter would refuse the value at \
5727 `helm lint` time far from the source caixa.lisp; the gate moves \
5728 the diagnostic to the manifest layer with the offending value \
5729 named verbatim)"
5730 )]
5731 LicencaInvalid { licenca: String, reason: String },
5732 #[error(
5733 ":edicao is the empty string (every published caixa names \
5734 its language edition via a non-empty `:edicao` value — the \
5735 edition determines the tatara-lisp macro surface + \
5736 compatibility flags the substrate applies when building \
5737 the caixa; the canonical `Caixa::template` scaffold every \
5738 `feira init` emits carries `:edicao \"2026\"` verbatim and \
5739 every renderer-side fixture (`caixa-helm`, `caixa-flux`, \
5740 `caixa-mesh`) carries `edicao: Some(\"2026\".into())` by \
5741 construction, so an empty `Some(\"\")` silently lands as a \
5742 bare `(:edicao \"\")` line in the rendered `caixa.lisp` and \
5743 a future renderer-side consumer that folds it through \
5744 `Option::unwrap_or_else` will skip the fallback and pass the \
5745 empty edition through to the substrate's build-time edition \
5746 selector far from the source caixa.lisp; omit the slot \
5747 entirely to defer to the substrate's default edition, or \
5748 carry a canonical edition like `\"2026\"`)"
5749 )]
5750 EdicaoEmpty,
5751 #[error(
5752 ":edicao {edicao:?} is not a valid edition: {reason} (every \
5753 documented tatara-lisp edition is a 4-digit ASCII decimal \
5754 year — `\"2026\"` is the only edition currently minted; \
5755 future-introduced siblings will follow the same shape, peer \
5756 with Cargo's `[package] edition` grammar which every value \
5757 Cargo has ever accepted matches: `\"2015\"`, `\"2018\"`, \
5758 `\"2021\"`, `\"2024\"`. Without this gate the canonical \
5759 paste-from-doc footguns silently passed: a trailing space \
5760 (`\"2026 \"`) from a paste-from-doc, a CRLF (`\"2026\\n\"`) \
5761 from a paste-from-multiline-doc, a fullwidth-keyboard \
5762 look-alike (`\"2026\"`), a free-form non-year value \
5763 (`\"x\"`, `\"latest\"`, `\"nightly\"`), a leading non-digit \
5764 version-tag prefix (`\"v2026\"`, `\"e2026\"`), a \
5765 decimal-shaped pseudo-version (`\"2026.1\"`), or a \
5766 wrong-length numeric value (`\"26\"`, `\"202\"`, \
5767 `\"20260\"`) all landed as `(:edicao \"<garbage>\")` in the \
5768 rendered caixa.lisp and broke at the substrate's \
5769 build-time edition selector far from the source caixa.lisp; \
5770 omit the slot entirely to defer to the substrate's default \
5771 edition, or carry a canonical 4-digit ASCII decimal year \
5772 like `\"2026\"`)"
5773 )]
5774 EdicaoInvalid { edicao: String, reason: String },
5775}
5776
5777#[cfg(test)]
5778mod tests {
5779 use super::*;
5780
5781 #[test]
5782 fn template_round_trips() {
5783 let src = Caixa::template("demo");
5784 let c = Caixa::from_lisp(&src).expect("template must parse");
5785 assert_eq!(c.nome, "demo");
5786 assert_eq!(c.versao, "0.1.0");
5787 assert_eq!(c.kind, CaixaKind::Biblioteca);
5788 assert_eq!(c.bibliotecas, vec!["lib/demo.lisp".to_string()]);
5789 assert!(c.deps.is_empty());
5790 assert!(c.deps_dev.is_empty());
5791 }
5792
5793 #[test]
5794 fn register_populates_registry() {
5795 Caixa::register().expect("first register call in this test process must succeed");
5796 let kws = tatara_lisp::domain::registered_keywords();
5797 assert!(kws.contains(&"defcaixa"));
5798 }
5799
5800 #[test]
5801 fn to_lisp_round_trips() {
5802 let src = Caixa::template("demo");
5803 let c1 = Caixa::from_lisp(&src).unwrap();
5804 let emitted = c1.to_lisp();
5805 let c2 = Caixa::from_lisp(&emitted).expect("emitted lisp parses back");
5806 assert_eq!(c1, c2);
5807 }
5808
5809 // ── DialetoEstrangeiro carries a single typed axis ────────────────────
5810 //
5811 // The compounding pin: the variant stores only the typed
5812 // [`crate::dialeto::CaixaDialeto`], and every user-facing byte-string
5813 // (canonical keyword, description, consumer) routes through the enum's
5814 // own accessors at Display time. Prior to that closure the variant
5815 // carried each accessor's return value as a stored `&'static str`
5816 // snapshot alongside `dialeto`; a caller could construct the variant
5817 // with a snapshot that drifted from what `dialeto`'s accessors would
5818 // return, and every downstream user-facing projection would silently
5819 // disagree with the classification. Storing only the axis makes the
5820 // drift structurally impossible.
5821
5822 #[test]
5823 fn dialeto_estrangeiro_variant_carries_only_the_typed_dialeto_axis() {
5824 // Single-field construction is the whole compounding shape — a
5825 // future re-introduction of a snapshot field (a `palavra_canonica:
5826 // &'static str`, a stored `descricao:`, a stored `consumidor:`)
5827 // would re-open the drift surface and this construction would fail
5828 // to compile with "missing field" until every snapshot was seeded
5829 // at the call site again. The compile-time guarantee is the
5830 // invariant; the assertion below only witnesses that the
5831 // construction is well-formed after the closure.
5832 let err = LeituraError::DialetoEstrangeiro {
5833 dialeto: crate::dialeto::CaixaDialeto::Molde,
5834 };
5835 assert!(matches!(
5836 err,
5837 LeituraError::DialetoEstrangeiro {
5838 dialeto: crate::dialeto::CaixaDialeto::Molde,
5839 }
5840 ));
5841 }
5842
5843 #[test]
5844 fn dialeto_estrangeiro_display_routes_through_typed_dialeto_accessors() {
5845 // For every foreign-dialect classification the variant surfaces —
5846 // [`crate::dialeto::CaixaDialeto::Molde`] and
5847 // [`crate::dialeto::CaixaDialeto::MoldePosicional`], the two
5848 // variants [`Caixa::from_lisp`] raises this error for — the
5849 // rendered [`std::fmt::Display`] byte-string must interpolate each
5850 // typed accessor's return verbatim. A future re-introduction of a
5851 // stored `&'static str` snapshot alongside `dialeto` that Display
5852 // read instead of the accessor would fail this pin as soon as the
5853 // two disagreed; a future accessor rebrand (a per-dialect
5854 // consumer rename, a canonical-keyword shift once the substrate
5855 // migration named in [`crate::dialeto`] completes) reaches every
5856 // consumer through one typed dispatch and this pin verifies the
5857 // display path is one of them.
5858 for d in [
5859 crate::dialeto::CaixaDialeto::Molde,
5860 crate::dialeto::CaixaDialeto::MoldePosicional,
5861 ] {
5862 let rendered = LeituraError::DialetoEstrangeiro { dialeto: d }.to_string();
5863 assert!(
5864 rendered.contains(d.palavra_canonica()),
5865 "Display must interpolate `dialeto.palavra_canonica()` \
5866 verbatim — a stored snapshot would silently drift from \
5867 the typed accessor. dialect: {d}, rendered: {rendered:?}"
5868 );
5869 assert!(
5870 rendered.contains(d.descricao()),
5871 "Display must interpolate `dialeto.descricao()` verbatim. \
5872 dialect: {d}, rendered: {rendered:?}"
5873 );
5874 assert!(
5875 rendered.contains(d.consumidor()),
5876 "Display must interpolate `dialeto.consumidor()` verbatim. \
5877 dialect: {d}, rendered: {rendered:?}"
5878 );
5879 }
5880 }
5881
5882 #[test]
5883 fn from_lisp_rejects_molde_dialect_via_typed_variant() {
5884 // The end-to-end pin the compounding closure defends: a
5885 // Molde-dialect source lands as [`LeituraError::DialetoEstrangeiro`]
5886 // carrying [`crate::dialeto::CaixaDialeto::Molde`], and the
5887 // rendered Display byte-string names the Molde accessors'
5888 // returns verbatim. Any future path that constructed the variant
5889 // with a mismatched snapshot (a stored `palavra_canonica:
5890 // "defcaixa"` on a `Molde` classification) would land Display
5891 // pointing at `defcaixa` while the typed axis said `Molde` — the
5892 // exact drift the closure removes.
5893 let src = r#"
5894 (defcaixa
5895 :name "x"
5896 :kind :Biblioteca
5897 :ecosystem :rust-single-crate
5898 :package {:name "x" :version "0.1.0"})
5899 "#;
5900 let err = Caixa::from_lisp(src).expect_err("Molde dialect must not parse as Pacote");
5901 match err {
5902 LeituraError::DialetoEstrangeiro { dialeto } => {
5903 assert_eq!(dialeto, crate::dialeto::CaixaDialeto::Molde);
5904 let rendered = LeituraError::DialetoEstrangeiro { dialeto }.to_string();
5905 assert!(rendered.contains(dialeto.palavra_canonica()));
5906 assert!(rendered.contains(dialeto.consumidor()));
5907 assert!(rendered.contains(dialeto.descricao()));
5908 }
5909 other => panic!("expected DialetoEstrangeiro, got {other:?}"),
5910 }
5911 }
5912
5913 #[test]
5914 fn from_lisp_rejects_molde_posicional_dialect_via_typed_variant() {
5915 // Coverage pin for the [`crate::dialeto::CaixaDialeto::MoldePosicional`]
5916 // arm of the [`Caixa::from_lisp`] foreign-dialect gate — the
5917 // positional-arity `defmolde` form written under a `(defcaixa …)`
5918 // head (`(defcaixa todoku-go :kind :Biblioteca :ecosystem :go
5919 // …)`). Pre-lift this arm rode the same `foreign =>` wildcard
5920 // the [`crate::dialeto::CaixaDialeto::Molde`] sibling arm rode,
5921 // so no test exercised the positional-arity path through
5922 // `Caixa::from_lisp` specifically; the sibling
5923 // [`from_lisp_rejects_molde_dialect_via_typed_variant`] only
5924 // covered [`crate::dialeto::CaixaDialeto::Molde`]. Post-lift the
5925 // two arms route through the lifted
5926 // [`crate::dialeto::CaixaDialeto::is_molde_family`] (e9d2315)
5927 // typed predicate — the same predicate the pre-lift `foreign =>`
5928 // wildcard resolved to today — and this pin makes the
5929 // positional-arity arm's byte-shape at the gate explicit rather
5930 // than implied by wildcard-absorption. A future regression that
5931 // silently reordered [`crate::dialeto::CaixaDialeto::is_molde_family`]'s
5932 // arm-set (dropped [`crate::dialeto::CaixaDialeto::MoldePosicional`]
5933 // from the two-arity closure) would fail this pin at caixa-core
5934 // test time rather than surfacing far from the change as a
5935 // `caixa.lisp` carrying a `(defcaixa todoku-go :ecosystem :go
5936 // …)` silently parsing past the derive.
5937 let src = r#"
5938 (defcaixa todoku-go
5939 :kind :Biblioteca
5940 :ecosystem :go
5941 :package {:name "todoku-go" :version "0.3.0"})
5942 "#;
5943 let err =
5944 Caixa::from_lisp(src).expect_err("MoldePosicional dialect must not parse as Pacote");
5945 match err {
5946 LeituraError::DialetoEstrangeiro { dialeto } => {
5947 assert_eq!(
5948 dialeto,
5949 crate::dialeto::CaixaDialeto::MoldePosicional,
5950 "DialetoEstrangeiro must carry the MoldePosicional \
5951 variant verbatim — the positional-arity `defmolde` \
5952 form under a `(defcaixa …)` head is the \
5953 `MoldePosicional` arm's canonical byte-shape"
5954 );
5955 let rendered = LeituraError::DialetoEstrangeiro { dialeto }.to_string();
5956 assert!(
5957 rendered.contains(dialeto.palavra_canonica()),
5958 "Display must interpolate `dialeto.palavra_canonica()` \
5959 verbatim on the MoldePosicional arm; rendered: \
5960 {rendered:?}"
5961 );
5962 assert!(
5963 rendered.contains(dialeto.consumidor()),
5964 "Display must interpolate `dialeto.consumidor()` \
5965 verbatim on the MoldePosicional arm; rendered: \
5966 {rendered:?}"
5967 );
5968 assert!(
5969 rendered.contains(dialeto.descricao()),
5970 "Display must interpolate `dialeto.descricao()` \
5971 verbatim on the MoldePosicional arm; rendered: \
5972 {rendered:?}"
5973 );
5974 }
5975 other => panic!("expected DialetoEstrangeiro, got {other:?}"),
5976 }
5977 }
5978
5979 #[test]
5980 fn from_lisp_dialect_gate_dispatches_through_caixa_dialeto_is_molde_family_predicate() {
5981 // Load-bearing byte-parity pin: for every arm in
5982 // [`crate::dialeto::CaixaDialeto::ALL`], the
5983 // [`Caixa::from_lisp`] foreign-dialect gate's DialetoEstrangeiro
5984 // partition must agree with the lifted
5985 // [`crate::dialeto::CaixaDialeto::is_molde_family`] (e9d2315)
5986 // typed predicate — i.e. from_lisp raises
5987 // [`LeituraError::DialetoEstrangeiro`] carrying `d` iff
5988 // `d.is_molde_family()` returns `true`, and does NOT raise
5989 // [`LeituraError::DialetoEstrangeiro`] on any arm where the
5990 // predicate returns `false` (the arm's source falls through to
5991 // the derive — parses cleanly on
5992 // [`crate::dialeto::CaixaDialeto::Pacote`], surfaces a
5993 // [`LeituraError::Leitura`] on
5994 // [`crate::dialeto::CaixaDialeto::Desconhecido`]).
5995 //
5996 // Pre-lift the gate hand-rolled a three-arm match
5997 // (`Pacote => {}`, `Desconhecido => {}`, `foreign => Err(…)`)
5998 // whose `foreign =>` wildcard expressed no compile-time link
5999 // back to the substrate primitive's arm-family; a future fifth
6000 // dialect the [`crate::dialeto`] module doc's "third dialect"
6001 // hazard actualises would fall silently onto the wildcard
6002 // regardless of whether it belonged to the `defmolde` family or
6003 // to a distinct `defcaixa`-family. Post-lift the partition
6004 // resolves through
6005 // [`crate::dialeto::CaixaDialeto::is_molde_family`]'s single
6006 // typed dispatch, and this pin refuses any future regression
6007 // that silently split the from_lisp partition from the typed
6008 // predicate — the two paths now migrate as one on any future
6009 // arm addition.
6010 //
6011 // Sibling in shape to the peer
6012 // [`crate::dialeto::tests::caixa_dialeto_is_molde_family_agrees_with_palavra_canonica_defmolde_projection`]
6013 // (e9d2315) that pins the same byte-parity between
6014 // [`crate::dialeto::CaixaDialeto::is_molde_family`] and the
6015 // sibling [`crate::dialeto::CaixaDialeto::palavra_canonica`]
6016 // `== "defmolde"` classifier — extends the discipline from the
6017 // two paths within the [`crate::dialeto`] primitive onto the
6018 // third external consumer of the `defmolde`-family partition
6019 // (the [`Caixa::from_lisp`] gate that raises
6020 // [`LeituraError::DialetoEstrangeiro`]).
6021 let fixtures: &[(crate::dialeto::CaixaDialeto, &str)] = &[
6022 (
6023 crate::dialeto::CaixaDialeto::Pacote,
6024 r#"
6025 (defcaixa
6026 :nome "checkout"
6027 :versao "0.1.0"
6028 :kind Biblioteca
6029 :edicao "2026"
6030 :descricao "canonical Pacote source"
6031 :autores ()
6032 :etiquetas ()
6033 :deps ()
6034 :deps-dev ()
6035 :bibliotecas ("lib/checkout.lisp"))
6036 "#,
6037 ),
6038 (
6039 crate::dialeto::CaixaDialeto::Molde,
6040 r#"
6041 (defcaixa
6042 :name "base64"
6043 :kind :Biblioteca
6044 :ecosystem :rust-single-crate
6045 :package {:name "base64" :version "0.22.1"}
6046 :workflows [:auto-release])
6047 "#,
6048 ),
6049 (
6050 crate::dialeto::CaixaDialeto::MoldePosicional,
6051 r#"
6052 (defcaixa todoku-go
6053 :kind :Biblioteca
6054 :ecosystem :go
6055 :package {:name "todoku-go" :version "0.3.0"})
6056 "#,
6057 ),
6058 (
6059 crate::dialeto::CaixaDialeto::Desconhecido,
6060 r#"(defcaixa :licenca "MIT")"#,
6061 ),
6062 ];
6063
6064 // Coverage: every arm in [`crate::dialeto::CaixaDialeto::ALL`]
6065 // must appear in the fixture table so the pin's arm-set stays
6066 // synchronised with the enum's arm-set. Fails at test time if a
6067 // future fifth arm added to [`crate::dialeto::CaixaDialeto`]
6068 // (with a corresponding `is_molde_family` return) forgot to
6069 // extend this fixture table with a canonical source for the new
6070 // arm — the pin cannot cover an arm it has no source for.
6071 for &expected in crate::dialeto::CaixaDialeto::ALL {
6072 assert!(
6073 fixtures.iter().any(|(d, _)| *d == expected),
6074 "fixture table must carry a canonical source for every \
6075 CaixaDialeto arm; missing: {expected:?}"
6076 );
6077 }
6078
6079 for &(expected_dialect, src) in fixtures {
6080 let classified = crate::dialeto::classify(src.trim()).unwrap_or_else(|err| {
6081 panic!(
6082 "fixture source for {expected_dialect:?} must classify \
6083 cleanly, got err: {err:?}"
6084 )
6085 });
6086 assert_eq!(
6087 classified, expected_dialect,
6088 "fixture source for {expected_dialect:?} must classify as \
6089 {expected_dialect:?} (drift here defeats the byte-parity \
6090 pin below — a source labelled for one arm but classifying \
6091 as another would silently satisfy or violate the pin for \
6092 the wrong reason)"
6093 );
6094
6095 let outcome = Caixa::from_lisp(src);
6096 match (expected_dialect.is_molde_family(), &outcome) {
6097 (true, Err(LeituraError::DialetoEstrangeiro { dialeto })) => {
6098 assert_eq!(
6099 *dialeto, expected_dialect,
6100 "DialetoEstrangeiro must carry the same typed arm \
6101 the classifier returned — a drift here would let \
6102 from_lisp raise the error while pointing at the \
6103 wrong dialect (e.g. rejecting a \
6104 MoldePosicional source as Molde). arm: \
6105 {expected_dialect:?}"
6106 );
6107 }
6108 (true, other) => panic!(
6109 "arm {expected_dialect:?} has is_molde_family() = true \
6110 so from_lisp must raise DialetoEstrangeiro carrying \
6111 {expected_dialect:?}; got: {other:?}"
6112 ),
6113 (false, Err(LeituraError::DialetoEstrangeiro { dialeto })) => panic!(
6114 "arm {expected_dialect:?} has is_molde_family() = false \
6115 so from_lisp must NOT raise DialetoEstrangeiro; got \
6116 one carrying: {dialeto:?}. This means the typed \
6117 predicate and the from_lisp partition disagree on \
6118 this arm — exactly the drift this pin refuses."
6119 ),
6120 (false, _) => {
6121 // A non-molde arm's source falls through to the
6122 // derive: Pacote sources parse to Ok(_); Desconhecido
6123 // sources surface as LeituraError::Leitura from the
6124 // derive's own unknown-keyword rejection. Either
6125 // shape is acceptable here — the pin's promise is
6126 // narrower: "no DialetoEstrangeiro on
6127 // is_molde_family() == false".
6128 }
6129 }
6130 }
6131 }
6132
6133 // ── M2 typed-substrate slot tests (limits, behavior, upgrade-from, supervisor) ──
6134
6135 #[test]
6136 fn limits_round_trip_via_json() {
6137 use crate::LimitsSpec;
6138 use std::time::Duration;
6139 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6140 c.limits = Some(LimitsSpec {
6141 memory: Some(64 * 1024 * 1024),
6142 fuel: Some(1_000_000),
6143 wall_clock: Some(Duration::from_secs(30)),
6144 cpu: Some(500),
6145 });
6146 let json = serde_json::to_string(&c).unwrap();
6147 assert!(json.contains("\"limits\""));
6148 assert!(json.contains("\"64MiB\""));
6149 assert!(json.contains("\"30s\""));
6150 assert!(json.contains("\"500m\""));
6151 let back: Caixa = serde_json::from_str(&json).unwrap();
6152 assert_eq!(c.limits, back.limits);
6153 }
6154
6155 #[test]
6156 fn behavior_round_trip_via_json() {
6157 use crate::BehaviorSpec;
6158 use std::path::PathBuf;
6159 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6160 c.behavior = Some(BehaviorSpec {
6161 on_init: Some(PathBuf::from("lib/init.lisp")),
6162 on_call: Some(PathBuf::from("lib/handlers.lisp")),
6163 ..Default::default()
6164 });
6165 let json = serde_json::to_string(&c).unwrap();
6166 let back: Caixa = serde_json::from_str(&json).unwrap();
6167 assert_eq!(c.behavior, back.behavior);
6168 }
6169
6170 #[test]
6171 fn upgrade_from_round_trip_via_json() {
6172 use crate::{UpgradeFromEntry, UpgradeInstruction};
6173 use std::path::PathBuf;
6174 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6175 c.upgrade_from = vec![UpgradeFromEntry {
6176 from: "0.1.0".into(),
6177 instructions: vec![
6178 UpgradeInstruction::LoadModule {
6179 module: "demo".into(),
6180 },
6181 UpgradeInstruction::StateChange {
6182 script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
6183 },
6184 UpgradeInstruction::SoftPurge {
6185 module: "demo-old".into(),
6186 },
6187 ],
6188 }];
6189 let json = serde_json::to_string(&c).unwrap();
6190 let back: Caixa = serde_json::from_str(&json).unwrap();
6191 assert_eq!(c.upgrade_from, back.upgrade_from);
6192 }
6193
6194 #[test]
6195 fn supervisor_view_returns_typed_shape() {
6196 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6197 let mut c = Caixa::from_lisp(&Caixa::template("root")).unwrap();
6198 c.kind = CaixaKind::Supervisor;
6199 c.bibliotecas.clear();
6200 c.estrategia = Some(RestartStrategy::OneForOne);
6201 c.max_restarts = Some(5);
6202 c.restart_window = Some("60s".into());
6203 c.children = vec![ChildSpec {
6204 caixa: "worker".into(),
6205 versao: "^0.1".into(),
6206 restart: RestartPolicy::Permanent,
6207 }];
6208 let view = c.supervisor_view().expect("Supervisor kind has a view");
6209 assert_eq!(view.estrategia, RestartStrategy::OneForOne);
6210 assert_eq!(view.max_restarts, 5);
6211 assert_eq!(
6212 view.restart_window,
6213 Some(std::time::Duration::from_secs(60))
6214 );
6215 assert_eq!(view.children.len(), 1);
6216 view.validate().unwrap();
6217 }
6218
6219 #[test]
6220 fn supervisor_view_none_for_non_supervisor_kinds() {
6221 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6222 assert!(c.supervisor_view().is_none());
6223 }
6224
6225 #[test]
6226 fn declared_mesh_slots_empty_for_bare_caixa() {
6227 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6228 assert!(c.declared_mesh_slots().is_empty());
6229 }
6230
6231 #[test]
6232 fn declared_mesh_slots_reports_only_set_slots_in_canonical_order() {
6233 use crate::{Entrada, Membro};
6234 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6235 // Set a non-adjacent pair (:membros + :entrada) to pin that the
6236 // canonical declaration order is preserved regardless of which
6237 // subset is populated.
6238 c.membros = vec![Membro {
6239 caixa: "a".into(),
6240 versao: "^0.1".into(),
6241 }];
6242 c.entrada = Some(Entrada {
6243 host: "x.example.com".into(),
6244 para: "a".into(),
6245 paths: vec![],
6246 port: 8080,
6247 });
6248 assert_eq!(
6249 c.declared_mesh_slots(),
6250 vec![
6251 crate::render::M3_AUTHOR_KEY_MEMBROS,
6252 crate::render::M3_AUTHOR_KEY_ENTRADA,
6253 ]
6254 );
6255 }
6256
6257 #[test]
6258 fn m3_top_level_author_key_consts_pin_canonical_kebab_case_labels() {
6259 // Scalar-value pin: the five author-facing kebab-case labels the
6260 // `(defcaixa … :<slot> (…))` surface admits on the M3 top-level
6261 // mesh slot axis, one arm per typed slot. Mirrors the peer
6262 // scalar-value pin the sibling
6263 // [`crate::M2_AUTHOR_KEY_LIMITS`] /
6264 // [`crate::M2_AUTHOR_KEY_BEHAVIOR`] /
6265 // [`crate::M2_AUTHOR_KEY_UPGRADE_FROM`] M2 top-level slot consts
6266 // carry (f49c8b0), so both altitudes of the typed-slot algebra
6267 // (per-Servico M2 + per-Aplicacao M3) share the same
6268 // "one canonical byte-string per arm" discipline. A future
6269 // rebrand (`:membros` → `:members`, `:contratos` → `:contracts`,
6270 // `:politicas` → `:policies`, `:placement` → `:distribution`,
6271 // `:entrada` → `:ingress`) lands as an edit to exactly one const,
6272 // and every consumer that reaches for the label picks it up at
6273 // build time rather than at runtime as a downstream mismatch.
6274 assert_eq!(crate::render::M3_AUTHOR_KEY_MEMBROS, ":membros");
6275 assert_eq!(crate::render::M3_AUTHOR_KEY_CONTRATOS, ":contratos");
6276 assert_eq!(crate::render::M3_AUTHOR_KEY_POLITICAS, ":politicas");
6277 assert_eq!(crate::render::M3_AUTHOR_KEY_PLACEMENT, ":placement");
6278 assert_eq!(crate::render::M3_AUTHOR_KEY_ENTRADA, ":entrada");
6279 }
6280
6281 #[test]
6282 fn declared_mesh_slots_route_through_lifted_m3_author_key_consts() {
6283 // Production-through-const pin: the five per-arm labels the
6284 // [`Caixa::declared_mesh_slots`] tagger pushes onto its return
6285 // `Vec` route through the lifted
6286 // [`crate::M3_AUTHOR_KEY_MEMBROS`] /
6287 // [`crate::M3_AUTHOR_KEY_CONTRATOS`] /
6288 // [`crate::M3_AUTHOR_KEY_POLITICAS`] /
6289 // [`crate::M3_AUTHOR_KEY_PLACEMENT`] /
6290 // [`crate::M3_AUTHOR_KEY_ENTRADA`] consts, in canonical
6291 // declaration order. A future re-order or drift at the tagger
6292 // (a rename that reaches the tagger but not the const, or vice
6293 // versa) surfaces here at build time rather than at runtime as
6294 // a [`crate::LayoutError::MeshSlotsOnNonAplicacao`]
6295 // `slots: <stale-kebab-case>` diagnostic far from the rename's
6296 // commit. Mirror of the peer
6297 // [`declared_servico_slots_route_through_lifted_m2_author_key_consts`]
6298 // pin (f49c8b0) on the sibling per-Servico M2 top-level slot
6299 // axis.
6300 use crate::{Entrada, Membro, MeshPolicy, Placement, PlacementStrategy, WitContract};
6301 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6302 c.membros = vec![Membro {
6303 caixa: "a".into(),
6304 versao: "^0.1".into(),
6305 }];
6306 c.contratos = vec![WitContract {
6307 de: "a".into(),
6308 para: "a".into(),
6309 wit: "wasi:http/proxy".into(),
6310 endpoint: Some("/x".into()),
6311 subject: None,
6312 slot: None,
6313 }];
6314 c.politicas = Some(MeshPolicy::default());
6315 c.placement = Some(Placement {
6316 estrategia: PlacementStrategy::Replicated,
6317 clusters: vec!["rio".into()],
6318 affinity: None,
6319 shard_key: None,
6320 });
6321 c.entrada = Some(Entrada {
6322 host: "x.example.com".into(),
6323 para: "a".into(),
6324 paths: vec![],
6325 port: 8080,
6326 });
6327 assert_eq!(
6328 c.declared_mesh_slots(),
6329 vec![
6330 crate::render::M3_AUTHOR_KEY_MEMBROS,
6331 crate::render::M3_AUTHOR_KEY_CONTRATOS,
6332 crate::render::M3_AUTHOR_KEY_POLITICAS,
6333 crate::render::M3_AUTHOR_KEY_PLACEMENT,
6334 crate::render::M3_AUTHOR_KEY_ENTRADA,
6335 ]
6336 );
6337 }
6338
6339 #[test]
6340 fn declared_supervisor_slots_empty_for_bare_caixa() {
6341 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6342 assert!(c.declared_supervisor_slots().is_empty());
6343 }
6344
6345 #[test]
6346 fn declared_supervisor_slots_reports_only_set_slots_in_canonical_order() {
6347 use crate::RestartStrategy;
6348 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6349 // Set a non-adjacent pair (:estrategia + :restart-window) to pin
6350 // that the canonical declaration order is preserved regardless
6351 // of which subset is populated.
6352 c.estrategia = Some(RestartStrategy::OneForOne);
6353 c.restart_window = Some("60s".into());
6354 assert_eq!(
6355 c.declared_supervisor_slots(),
6356 vec![
6357 crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA,
6358 crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW,
6359 ]
6360 );
6361 }
6362
6363 #[test]
6364 fn supervisor_top_level_author_key_consts_pin_canonical_kebab_case_labels() {
6365 // Scalar-value pin: the four author-facing kebab-case labels the
6366 // `(defcaixa … :<slot> (…))` surface admits on the Supervisor
6367 // supervision-tree slot axis, one arm per typed slot. Mirrors the
6368 // peer scalar-value pins the sibling
6369 // [`crate::render::M2_AUTHOR_KEY_LIMITS`] /
6370 // [`crate::render::M2_AUTHOR_KEY_BEHAVIOR`] /
6371 // [`crate::render::M2_AUTHOR_KEY_UPGRADE_FROM`] top-level M2 slot
6372 // consts and [`crate::render::M3_AUTHOR_KEY_MEMBROS`] etc.
6373 // top-level M3 slot consts carry, so all three kind-scoped
6374 // typed-slot-family author-facing-label axes route through one
6375 // canonical per-arm declaration. A future rebrand
6376 // (`:estrategia` → `:strategy` for English uniformity,
6377 // `:max-restarts` → `:max-intensity` matching Erlang/OTP's
6378 // `MaxIntensity` name, `:restart-window` → `:period` matching
6379 // OTP's `Period` name, `:children` → `:workers` matching Elixir
6380 // idiom) lands as an edit to exactly one const, and every
6381 // consumer that reaches for the label picks it up at build time
6382 // rather than at runtime as a downstream mismatch.
6383 assert_eq!(
6384 crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA,
6385 ":estrategia"
6386 );
6387 assert_eq!(
6388 crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS,
6389 ":max-restarts"
6390 );
6391 assert_eq!(
6392 crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW,
6393 ":restart-window"
6394 );
6395 assert_eq!(crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN, ":children");
6396 }
6397
6398 #[test]
6399 fn declared_supervisor_slots_route_through_lifted_supervisor_author_key_consts() {
6400 // Production-through-const pin: the four per-arm labels the
6401 // [`Caixa::declared_supervisor_slots`] tagger pushes onto its
6402 // return `Vec` route through the lifted
6403 // [`crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA`] /
6404 // [`crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS`] /
6405 // [`crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW`] /
6406 // [`crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN`] consts, in
6407 // canonical declaration order. A future re-order or drift at the
6408 // tagger (a rename that reaches the tagger but not the const, or
6409 // vice versa) surfaces here at build time rather than at runtime
6410 // as a [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
6411 // `slots: <stale-kebab-case>` diagnostic far from the rename's
6412 // commit. Mirror of the peer
6413 // [`declared_servico_slots_route_through_lifted_m2_author_key_consts`]
6414 // (f49c8b0) and
6415 // [`declared_mesh_slots_route_through_lifted_m3_author_key_consts`]
6416 // (882f498) pins on the sibling M2 / M3 top-level slot axes.
6417 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6418 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6419 c.estrategia = Some(RestartStrategy::OneForOne);
6420 c.max_restarts = Some(5);
6421 c.restart_window = Some("60s".into());
6422 c.children = vec![ChildSpec {
6423 caixa: "worker".into(),
6424 versao: "^0.1".into(),
6425 restart: RestartPolicy::Permanent,
6426 }];
6427 assert_eq!(
6428 c.declared_supervisor_slots(),
6429 vec![
6430 crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA,
6431 crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS,
6432 crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW,
6433 crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN,
6434 ]
6435 );
6436 }
6437
6438 #[test]
6439 fn declared_servico_slots_empty_for_bare_caixa() {
6440 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6441 assert!(c.declared_servico_slots().is_empty());
6442 }
6443
6444 #[test]
6445 fn declared_servico_slots_reports_only_set_slots_in_canonical_order() {
6446 use crate::{UpgradeFromEntry, UpgradeInstruction};
6447 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6448 // Set a non-adjacent pair (:limits + :upgrade-from) to pin that
6449 // the canonical declaration order is preserved regardless of
6450 // which subset is populated.
6451 c.limits = Some(crate::LimitsSpec {
6452 fuel: Some(1_000_000),
6453 ..Default::default()
6454 });
6455 c.upgrade_from = vec![UpgradeFromEntry {
6456 from: "0.1.0".into(),
6457 instructions: vec![UpgradeInstruction::Restart],
6458 }];
6459 assert_eq!(
6460 c.declared_servico_slots(),
6461 vec![
6462 crate::render::M2_AUTHOR_KEY_LIMITS,
6463 crate::render::M2_AUTHOR_KEY_UPGRADE_FROM,
6464 ]
6465 );
6466 }
6467
6468 #[test]
6469 fn m2_top_level_author_key_consts_pin_canonical_kebab_case_labels() {
6470 // Scalar-value pin: the three author-facing kebab-case labels
6471 // the `(defcaixa … :<slot> (…))` surface admits on the M2
6472 // top-level slot axis, one arm per typed slot. Mirrors the peer
6473 // scalar-value pin the sibling renderer-side
6474 // [`crate::M2_KEY_LIMITS`] / [`crate::M2_KEY_BEHAVIOR`] /
6475 // [`crate::M2_KEY_UPGRADE_FROM`] camelCase overlay-container
6476 // consts carry, so both halves of the M2 top-level slot dual
6477 // axis (author-facing kebab-case label + renderer-side
6478 // camelCase overlay-container wire key) route through one
6479 // canonical per-arm declaration. A future rebrand
6480 // (`:limits` → `:sandbox` matching Lunatic per-process
6481 // terminology INSPIRATIONS §III.1, `:behavior` → `:gen-server`
6482 // matching Erlang's verbatim name, `:upgrade-from` → `:appup`
6483 // matching Erlang's verbatim appup name) lands as an edit to
6484 // exactly one const, and every consumer that reaches for the
6485 // label picks it up at build time rather than at runtime as a
6486 // downstream mismatch.
6487 assert_eq!(crate::render::M2_AUTHOR_KEY_LIMITS, ":limits");
6488 assert_eq!(crate::render::M2_AUTHOR_KEY_BEHAVIOR, ":behavior");
6489 assert_eq!(crate::render::M2_AUTHOR_KEY_UPGRADE_FROM, ":upgrade-from");
6490 }
6491
6492 #[test]
6493 fn declared_servico_slots_route_through_lifted_m2_author_key_consts() {
6494 // Production-through-const pin: the three per-arm labels the
6495 // [`Caixa::declared_servico_slots`] tagger pushes onto its
6496 // return `Vec` route through the lifted
6497 // [`crate::M2_AUTHOR_KEY_LIMITS`] /
6498 // [`crate::M2_AUTHOR_KEY_BEHAVIOR`] /
6499 // [`crate::M2_AUTHOR_KEY_UPGRADE_FROM`] consts, in canonical
6500 // declaration order. A future re-order or drift at the tagger
6501 // (a rename that reaches the tagger but not the const, or vice
6502 // versa) surfaces here at build time rather than at runtime as
6503 // a [`crate::LayoutError::ServicoSlotsOnNonServico`]
6504 // `slots: <stale-kebab-case>` diagnostic far from the rename's
6505 // commit. Mirror of the peer
6506 // [`crate::behavior::BehaviorSpec::declared_slots`] production
6507 // tagger pin (889dc18) on the sibling per-callback axis.
6508 use crate::{BehaviorSpec, UpgradeFromEntry, UpgradeInstruction};
6509 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6510 c.limits = Some(crate::LimitsSpec {
6511 fuel: Some(1_000_000),
6512 ..Default::default()
6513 });
6514 c.behavior = Some(BehaviorSpec {
6515 on_init: Some(PathBuf::from("lib/init.lisp")),
6516 ..Default::default()
6517 });
6518 c.upgrade_from = vec![UpgradeFromEntry {
6519 from: "0.1.0".into(),
6520 instructions: vec![UpgradeInstruction::Restart],
6521 }];
6522 assert_eq!(
6523 c.declared_servico_slots(),
6524 vec![
6525 crate::render::M2_AUTHOR_KEY_LIMITS,
6526 crate::render::M2_AUTHOR_KEY_BEHAVIOR,
6527 crate::render::M2_AUTHOR_KEY_UPGRADE_FROM,
6528 ]
6529 );
6530 }
6531
6532 #[test]
6533 fn existing_manifests_unaffected_by_new_optional_slots() {
6534 // Regression test: a caixa.lisp authored before M2 typed slots
6535 // should still parse + serialize cleanly. The bare `defcaixa`
6536 // emitted by `Caixa::template` has none of the new fields.
6537 let src = Caixa::template("legacy");
6538 let c = Caixa::from_lisp(&src).unwrap();
6539 assert!(c.limits.is_none());
6540 assert!(c.behavior.is_none());
6541 assert!(c.upgrade_from.is_empty());
6542 assert!(c.estrategia.is_none());
6543 assert!(c.children.is_empty());
6544
6545 // And to_lisp emits a manifest with the new slots in the
6546 // empty/default state — round-trippable.
6547 let emitted = c.to_lisp();
6548 let back = Caixa::from_lisp(&emitted).unwrap();
6549 assert_eq!(c, back);
6550 }
6551
6552 #[test]
6553 fn validate_deps_accepts_canonical_caixa() {
6554 // Positive control: the bare template — zero deps, zero
6555 // deps_dev — passes the gate trivially. A future axis added to
6556 // `Dep::validate` mustn't regress an empty-deps caixa to a
6557 // build error.
6558 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6559 c.validate_deps().unwrap();
6560 }
6561
6562 #[test]
6563 fn validate_deps_rejects_invalid_versao_in_deps() {
6564 // Fail-before-pass-after pin: a malformed `:deps :versao`
6565 // surfaces at validate_deps() time, not at lacre-resolve time.
6566 // Mirrors `rejects_invalid_membro_versao_requirement` and
6567 // `validate_rejects_invalid_child_versao_requirement` on the
6568 // other two `:versao` axes.
6569 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6570 c.deps = vec![Dep::simple("caixa-teia", "^bad-version")];
6571 let err = c.validate_deps().unwrap_err();
6572 assert!(
6573 matches!(
6574 err,
6575 crate::dep::DepError::VersaoInvalid { ref nome, ref versao, .. }
6576 if nome == "caixa-teia" && versao == "^bad-version"
6577 ),
6578 "got {err:?}"
6579 );
6580 }
6581
6582 #[test]
6583 fn validate_deps_rejects_invalid_versao_in_deps_dev() {
6584 // Parity pin: `:deps-dev` must run through the same per-entry
6585 // validator as `:deps` — a typo in either axis surfaces the
6586 // same diagnostic. Without this leg, `:deps-dev` would be a
6587 // second-class citizen of the typed surface and an author
6588 // could land a build that passes validate_deps but fails at
6589 // `feira lock`-time when the dev-dep is resolved for a test
6590 // build.
6591 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6592 c.deps_dev = vec![Dep::simple("tatara-check", "^^0.1")];
6593 let err = c.validate_deps().unwrap_err();
6594 assert!(
6595 matches!(
6596 err,
6597 crate::dep::DepError::VersaoInvalid { ref nome, ref versao, .. }
6598 if nome == "tatara-check" && versao == "^^0.1"
6599 ),
6600 "got {err:?}"
6601 );
6602 }
6603
6604 #[test]
6605 fn validate_deps_runs_deps_before_deps_dev() {
6606 // Order pin: when both lists carry typos, the `:deps`
6607 // diagnostic surfaces first. The author's mental model is
6608 // "runtime deps are load-bearing; dev deps are scaffolding";
6609 // surfacing the runtime axis first matches that hierarchy.
6610 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6611 c.deps = vec![Dep::simple("runtime-dep", "^bad-runtime")];
6612 c.deps_dev = vec![Dep::simple("dev-dep", "^bad-dev")];
6613 let err = c.validate_deps().unwrap_err();
6614 assert!(
6615 matches!(
6616 err,
6617 crate::dep::DepError::VersaoInvalid { ref nome, .. }
6618 if nome == "runtime-dep"
6619 ),
6620 "expected `:deps` typo to surface first, got {err:?}"
6621 );
6622 }
6623
6624 #[test]
6625 fn validate_deps_accepts_canonical_versao_forms_in_both_lists() {
6626 // Positive control sweep across both lists. Pin every
6627 // canonical Cargo-shaped form so a future tightening of the
6628 // accepted set surfaces here as a test failure (parity with
6629 // `accepts_canonical_membro_versao_forms` and
6630 // `validate_accepts_canonical_child_versao_forms`).
6631 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6632 c.deps = vec![
6633 Dep::simple("caret", "^0.1"),
6634 Dep::simple("tilde", "~0.1.2"),
6635 Dep::simple("exact", "0.1.0"),
6636 Dep::simple("wildcard", "*"),
6637 Dep::simple("multi-range", ">=0.1, <2"),
6638 ];
6639 c.deps_dev = vec![
6640 Dep::simple("dev-caret", "^0.1"),
6641 Dep::simple("dev-wildcard", "*"),
6642 ];
6643 c.validate_deps().unwrap();
6644 }
6645
6646 #[test]
6647 fn validate_deps_diagnostic_carries_offending_dep() {
6648 // Diagnostic-shape pin: the error names the offending entry's
6649 // `:nome` + `:versao` verbatim and carries a non-empty
6650 // `reason` from `semver::VersionReq::parse`, so a `feira lint`
6651 // run can render the diagnostic without re-parsing.
6652 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6653 c.deps = vec![Dep::simple("caixa-teia", "not-a-req")];
6654 let err = c.validate_deps().unwrap_err();
6655 let crate::dep::DepError::VersaoInvalid {
6656 nome,
6657 versao,
6658 reason,
6659 } = err
6660 else {
6661 panic!("expected VersaoInvalid, got other variant");
6662 };
6663 assert_eq!(nome, "caixa-teia");
6664 assert_eq!(versao, "not-a-req");
6665 assert!(
6666 !reason.is_empty(),
6667 "VersaoInvalid `reason` must carry the parser's wording verbatim"
6668 );
6669 }
6670
6671 #[test]
6672 fn validate_deps_rejects_ambiguous_fonte_in_deps_dev() {
6673 // Cross-axis pin: `validate_deps` walks both :deps and
6674 // :deps-dev through `Dep::validate`, and the new fonte gate
6675 // (`:tag` + `:branch` both set — the canonical "pin drift"
6676 // footgun) must surface from the :deps-dev arm with the
6677 // offending entry's :nome named. Pin the :deps-dev arm
6678 // explicitly so a future shortcut that only walks :deps
6679 // surfaces here as a regression.
6680 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6681 c.deps_dev = vec![Dep {
6682 nome: "dev-only".into(),
6683 versao: "^0.1".into(),
6684 fonte: Some(crate::DepSource::Git {
6685 repo: "github:p/x".into(),
6686 tag: Some("v1".into()),
6687 rev: None,
6688 branch: Some("main".into()),
6689 }),
6690 opcional: false,
6691 caracteristicas: vec![],
6692 }];
6693 let err = c.validate_deps().unwrap_err();
6694 let crate::dep::DepError::FontePinAmbiguous { nome, pins } = err else {
6695 panic!("expected FontePinAmbiguous from :deps-dev walk");
6696 };
6697 assert_eq!(nome, "dev-only");
6698 assert!(pins.contains(":tag") && pins.contains(":branch"));
6699 }
6700
6701 #[test]
6702 fn validate_deps_rejects_empty_repo_in_deps() {
6703 // Parity pin on the :deps arm: an empty :repo on the runtime
6704 // deps list surfaces the same FonteRepoEmpty diagnostic the
6705 // dep.rs per-entry tests pin, naming the offending entry.
6706 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6707 c.deps = vec![Dep {
6708 nome: "runtime".into(),
6709 versao: "^0.1".into(),
6710 fonte: Some(crate::DepSource::Git {
6711 repo: String::new(),
6712 tag: Some("v1".into()),
6713 rev: None,
6714 branch: None,
6715 }),
6716 opcional: false,
6717 caracteristicas: vec![],
6718 }];
6719 let err = c.validate_deps().unwrap_err();
6720 assert!(
6721 matches!(
6722 err,
6723 crate::dep::DepError::FonteRepoEmpty { ref nome }
6724 if nome == "runtime"
6725 ),
6726 "got {err:?}"
6727 );
6728 }
6729
6730 // ── validate_deps: within-list :nome set-not-multiset gate ─────────
6731
6732 #[test]
6733 fn validate_deps_rejects_duplicate_nome_in_deps() {
6734 // Fail-before-pass-after pin: two `:deps` entries naming the same
6735 // caixa carry two `:versao` / `:fonte` / feature triples that the
6736 // caixa-resolver's lacre pipeline collapses (the second silently
6737 // overwrites the first at `concrete_versao`-resolve time). The
6738 // gate surfaces the duplicate at validate-time, naming the
6739 // offending caixa + the list, before the resolver-side silent
6740 // drop. Mirrors the peer typed-graph duplicate gates
6741 // (`DuplicateChildCaixa`, `MembroDuplicate`, `DuplicateFrom`, …).
6742 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6743 c.deps = vec![
6744 Dep::simple("caixa-teia", "^0.1"),
6745 Dep::simple("caixa-teia", "^0.2"),
6746 ];
6747 let err = c.validate_deps().unwrap_err();
6748 assert!(
6749 matches!(
6750 err,
6751 crate::dep::DepError::DuplicateNome { ref nome, list }
6752 if nome == "caixa-teia" && list == crate::render::DEP_AUTHOR_KEY_DEPS
6753 ),
6754 "got {err:?}"
6755 );
6756 }
6757
6758 #[test]
6759 fn validate_deps_rejects_duplicate_nome_in_deps_dev() {
6760 // Parity pin: `:deps-dev` runs through the same per-list
6761 // duplicate check as `:deps` — neither axis is a second-class
6762 // citizen of the set-not-multiset discipline.
6763 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6764 c.deps_dev = vec![
6765 Dep::simple("tatara-check", "*"),
6766 Dep::simple("tatara-check", "^0.1"),
6767 ];
6768 let err = c.validate_deps().unwrap_err();
6769 assert!(
6770 matches!(
6771 err,
6772 crate::dep::DepError::DuplicateNome { ref nome, list }
6773 if nome == "tatara-check" && list == crate::render::DEP_AUTHOR_KEY_DEPS_DEV
6774 ),
6775 "got {err:?}"
6776 );
6777 }
6778
6779 #[test]
6780 fn validate_deps_accepts_cross_list_same_nome() {
6781 // The Cargo `[dependencies]` + `[dev-dependencies]` override
6782 // convention is preserved: a name appearing in *both* lists is
6783 // valid (the dev-pin overrides at test/dev time). Only
6784 // within-list duplicates are structurally incoherent — pin the
6785 // permissive cross-list semantics so a future shortcut that
6786 // collapses the two seen-sets into one surfaces here as a test
6787 // failure.
6788 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6789 c.deps = vec![Dep::simple("caixa-teia", "^0.1")];
6790 c.deps_dev = vec![Dep::simple("caixa-teia", "^0.2")];
6791 c.validate_deps().unwrap();
6792 }
6793
6794 #[test]
6795 fn validate_deps_accepts_distinct_nome_in_both_lists() {
6796 // Positive control: distinct names within each list pass — the
6797 // gate's identity element on the canonical authoring shape.
6798 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6799 c.deps = vec![
6800 Dep::simple("caixa-teia", "^0.1"),
6801 Dep::simple("pleme-mesh", "*"),
6802 ];
6803 c.deps_dev = vec![
6804 Dep::simple("tatara-check", "*"),
6805 Dep::simple("dev-shim", "^0.1"),
6806 ];
6807 c.validate_deps().unwrap();
6808 }
6809
6810 #[test]
6811 fn validate_deps_per_entry_validate_fires_before_duplicate_in_deps() {
6812 // Diagnostic-precedence pin: a malformed `:versao` on the
6813 // duplicating entry surfaces its narrower `VersaoInvalid`
6814 // diagnostic first, before the cross-entry duplicate gate fires
6815 // — the canonical "per-entry shape before cross-entry uniqueness"
6816 // precedence every peer set-not-multiset gate establishes
6817 // (`*_invalid_fires_before_duplicate_check` pins on
6818 // `SupervisorSpec::validate`, `AplicacaoSpec::validate_membros`,
6819 // `validate_upgrade_from`).
6820 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6821 c.deps = vec![
6822 Dep::simple("caixa-teia", "^0.1"),
6823 Dep::simple("caixa-teia", "^bad-version"),
6824 ];
6825 let err = c.validate_deps().unwrap_err();
6826 assert!(
6827 matches!(
6828 err,
6829 crate::dep::DepError::VersaoInvalid { ref nome, ref versao, .. }
6830 if nome == "caixa-teia" && versao == "^bad-version"
6831 ),
6832 "expected VersaoInvalid to surface before DuplicateNome, got {err:?}"
6833 );
6834 }
6835
6836 #[test]
6837 fn validate_deps_duplicate_diagnostic_names_first_collision() {
6838 // First-collision determinism pin: with three entries naming the
6839 // same caixa, the first colliding pair surfaces — not the last.
6840 // Mirrors the peer first-collision posture on every
6841 // duplicate-target gate
6842 // (`validate_upgrade_from_duplicate_diagnostic_names_second_collision`
6843 // — the second entry is the first collision; this gate uses the
6844 // same shape: the second entry's `:nome` lands in the diagnostic
6845 // because `seen.insert(first.nome)` already populated the set).
6846 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6847 c.deps = vec![
6848 Dep::simple("caixa-teia", "^0.1"),
6849 Dep::simple("caixa-teia", "^0.2"),
6850 Dep::simple("caixa-teia", "^0.3"),
6851 ];
6852 let err = c.validate_deps().unwrap_err();
6853 // The diagnostic carries the offending caixa name; the
6854 // implementation surfaces on the *second* entry (the first
6855 // collision), so the test pins the `:nome` value.
6856 assert!(
6857 matches!(
6858 err,
6859 crate::dep::DepError::DuplicateNome { ref nome, list }
6860 if nome == "caixa-teia" && list == crate::render::DEP_AUTHOR_KEY_DEPS
6861 ),
6862 "got {err:?}"
6863 );
6864 }
6865
6866 #[test]
6867 fn validate_deps_duplicate_in_deps_fires_before_duplicate_in_deps_dev() {
6868 // Cross-list precedence pin: when both lists carry duplicates,
6869 // the `:deps` diagnostic surfaces first — same author-mental-
6870 // model ordering the `validate_deps_runs_deps_before_deps_dev`
6871 // pin establishes for malformed `:versao` (runtime axis before
6872 // dev axis).
6873 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6874 c.deps = vec![
6875 Dep::simple("runtime-dep", "^0.1"),
6876 Dep::simple("runtime-dep", "^0.2"),
6877 ];
6878 c.deps_dev = vec![Dep::simple("dev-dep", "*"), Dep::simple("dev-dep", "^0.1")];
6879 let err = c.validate_deps().unwrap_err();
6880 assert!(
6881 matches!(
6882 err,
6883 crate::dep::DepError::DuplicateNome { ref nome, list }
6884 if nome == "runtime-dep" && list == crate::render::DEP_AUTHOR_KEY_DEPS
6885 ),
6886 "expected :deps duplicate to surface before :deps-dev duplicate, got {err:?}"
6887 );
6888 }
6889
6890 #[test]
6891 fn validate_deps_empty_lists_pass_duplicate_gate() {
6892 // Empty-set identity pin: the bare template (zero deps, zero
6893 // deps_dev) passes the duplicate gate as the gate's identity
6894 // element. A future tighten that conflates "empty" with
6895 // "missing" would regress this baseline.
6896 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6897 c.validate_deps().unwrap();
6898 }
6899
6900 #[test]
6901 fn validate_deps_duplicate_diagnostic_carries_list_tag() {
6902 // Diagnostic-shape pin: the `list:` field tags which list the
6903 // duplicate landed in (`:deps` vs `:deps-dev`) verbatim, so a
6904 // `feira lint` run can route the author to the right block in
6905 // their caixa.lisp without re-deriving the list from context.
6906 // Same self-locating shape every peer per-axis diagnostic
6907 // already exposes.
6908 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6909 c.deps_dev = vec![
6910 Dep::simple("dev-thing", "*"),
6911 Dep::simple("dev-thing", "^0.1"),
6912 ];
6913 let err = c.validate_deps().unwrap_err();
6914 let crate::dep::DepError::DuplicateNome { nome, list } = err else {
6915 panic!("expected DuplicateNome from :deps-dev walk");
6916 };
6917 assert_eq!(nome, "dev-thing");
6918 assert_eq!(list, crate::render::DEP_AUTHOR_KEY_DEPS_DEV);
6919 }
6920
6921 // ── validate_deps: per-entry :caracteristicas set-discipline gate ──
6922
6923 #[test]
6924 fn validate_deps_surfaces_caracteristicas_duplicate_in_deps_list() {
6925 // Thread-through pin on `:deps`: the per-entry
6926 // `Dep::validate_caracteristicas` gate fires inside
6927 // `Caixa::validate_deps`'s linear walk, so a malformed feature
6928 // list on any `:deps` entry surfaces as a `DepError` from
6929 // `validate_deps` — the same reachability shape every per-entry
6930 // `Dep::validate` arm threads through. Without this pin a future
6931 // shortcut that skips the per-entry `Dep::validate` call on the
6932 // cross-entry-uniqueness path would mask the within-entry
6933 // `:caracteristicas` gates.
6934 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6935 c.deps = vec![Dep {
6936 nome: "caixa-teia".into(),
6937 versao: "^0.1".into(),
6938 fonte: None,
6939 opcional: false,
6940 caracteristicas: vec!["http".into(), "http".into()],
6941 }];
6942 let err = c.validate_deps().unwrap_err();
6943 let crate::dep::DepError::CaracteristicaDuplicate {
6944 nome,
6945 caracteristica,
6946 } = err
6947 else {
6948 panic!("expected CaracteristicaDuplicate from :deps walk, got {err:?}");
6949 };
6950 assert_eq!(nome, "caixa-teia");
6951 assert_eq!(caracteristica, "http");
6952 }
6953
6954 #[test]
6955 fn validate_deps_surfaces_caracteristicas_empty_in_deps_dev_list() {
6956 // Peer thread-through pin on `:deps-dev`: same reachability as
6957 // the `:deps` arm above, on the dev-only authoring axis. Pins
6958 // that the `validate_deps` walk visits both lists' per-entry
6959 // gates uniformly. The empty-feature arm carries here so both
6960 // new `:caracteristicas` arms are surfaced via at least one
6961 // `validate_deps` thread-through.
6962 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6963 c.deps_dev = vec![Dep {
6964 nome: "caixa-teia".into(),
6965 versao: "^0.1".into(),
6966 fonte: None,
6967 opcional: false,
6968 caracteristicas: vec![String::new()],
6969 }];
6970 let err = c.validate_deps().unwrap_err();
6971 let crate::dep::DepError::CaracteristicaEmpty { nome } = err else {
6972 panic!("expected CaracteristicaEmpty from :deps-dev walk, got {err:?}");
6973 };
6974 assert_eq!(nome, "caixa-teia");
6975 }
6976
6977 #[test]
6978 fn validate_deps_surfaces_caracteristicas_invalid_in_deps_list() {
6979 // Thread-through pin on `:deps`: the per-entry
6980 // `Dep::validate_caracteristicas` value-shape gate (lifted via
6981 // `crate::render::is_cargo_feature_name`) fires inside
6982 // `Caixa::validate_deps`'s linear walk on the `:deps` list, so
6983 // a structurally invalid feature name on any `:deps` entry
6984 // surfaces as `DepError::CaracteristicaInvalid` from
6985 // `validate_deps` — the same reachability shape every per-entry
6986 // `Dep::validate` arm threads through. Without this pin a
6987 // future shortcut that skips the per-entry `Dep::validate` call
6988 // on the cross-entry-uniqueness path would mask the within-
6989 // entry `:caracteristicas` value-shape gate.
6990 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6991 c.deps = vec![Dep {
6992 nome: "caixa-teia".into(),
6993 versao: "^0.1".into(),
6994 fonte: None,
6995 opcional: false,
6996 caracteristicas: vec!["+http".into()],
6997 }];
6998 let err = c.validate_deps().unwrap_err();
6999 let crate::dep::DepError::CaracteristicaInvalid {
7000 nome,
7001 caracteristica,
7002 ..
7003 } = err
7004 else {
7005 panic!("expected CaracteristicaInvalid from :deps walk, got {err:?}");
7006 };
7007 assert_eq!(nome, "caixa-teia");
7008 assert_eq!(caracteristica, "+http");
7009 }
7010
7011 #[test]
7012 fn validate_deps_surfaces_caracteristicas_invalid_in_deps_dev_list() {
7013 // Peer thread-through pin on `:deps-dev`: same reachability as
7014 // the `:deps` arm above, on the dev-only authoring axis. The
7015 // `http/json` shape carries here so the segment-separator
7016 // diagnostic (the canonical Cargo `dep/feat` namespaced-dep
7017 // confusion footgun) is surfaced via the cross-entry walk too —
7018 // pinning that the `:deps-dev` list visits the same per-entry
7019 // value-shape gate as the `:deps` list.
7020 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
7021 c.deps_dev = vec![Dep {
7022 nome: "caixa-teia".into(),
7023 versao: "^0.1".into(),
7024 fonte: None,
7025 opcional: false,
7026 caracteristicas: vec!["http/json".into()],
7027 }];
7028 let err = c.validate_deps().unwrap_err();
7029 let crate::dep::DepError::CaracteristicaInvalid {
7030 nome,
7031 caracteristica,
7032 ..
7033 } = err
7034 else {
7035 panic!("expected CaracteristicaInvalid from :deps-dev walk, got {err:?}");
7036 };
7037 assert_eq!(nome, "caixa-teia");
7038 assert_eq!(caracteristica, "http/json");
7039 }
7040
7041 #[test]
7042 fn to_lisp_preserves_deps() {
7043 let src = r#"
7044(defcaixa
7045 :nome "x"
7046 :versao "0.1.0"
7047 :kind Biblioteca
7048 :deps ((:nome "a" :versao "^0.1")
7049 (:nome "b" :versao "*" :fonte (:tipo git :repo "github:o/b" :tag "v1"))))
7050"#;
7051 let c1 = Caixa::from_lisp(src).unwrap();
7052 let emitted = c1.to_lisp();
7053 let c2 = Caixa::from_lisp(&emitted).expect("round trip");
7054 assert_eq!(c1.deps, c2.deps);
7055 }
7056
7057 // ── Caixa::validate_nome — top-level :nome value-shape gate ─────────
7058
7059 fn caixa_with_nome(nome: &str) -> Caixa {
7060 let mut c = Caixa::from_lisp(&Caixa::template("placeholder")).unwrap();
7061 c.nome = nome.to_string();
7062 c
7063 }
7064
7065 #[test]
7066 fn validate_nome_accepts_canonical_template() {
7067 // Positive control: the bare `feira init`-style template's
7068 // `:nome` ("demo") is a canonical DNS-1123 label; the gate must
7069 // not regress this baseline shape. A future tightening of the
7070 // accepted set surfaces here as a test failure first.
7071 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
7072 c.validate_nome().unwrap();
7073 }
7074
7075 #[test]
7076 fn validate_nome_accepts_canonical_forms() {
7077 // Positive-set sweep: each realistic caixa-name shape the K8s
7078 // apiserver accepts as a `metadata.name` label must pass —
7079 // single-word, hyphen-joined, version-suffixed, single-char,
7080 // two-char, digit-start (DNS-1123 allows this; the stricter
7081 // DNS-1035 Service-name rule doesn't), version-suffix-bearing.
7082 // Mirrors `accepts_canonical_membro_caixa_forms` (3f9d7a0) on
7083 // the peer member-name axis.
7084 for nome in [
7085 "checkout",
7086 "cart-v2",
7087 "a",
7088 "db",
7089 "3rd-party-shim",
7090 "payment-retry",
7091 "0",
7092 ] {
7093 caixa_with_nome(nome)
7094 .validate_nome()
7095 .unwrap_or_else(|e| panic!("canonical :nome {nome:?} must validate, got {e:?}"));
7096 }
7097 }
7098
7099 #[test]
7100 fn validate_nome_rejects_empty() {
7101 // Fail-before-pass-after pin: `Caixa::from_lisp` does not refuse
7102 // an empty `:nome` (the derive macro stores the raw String);
7103 // the gate's empty arm names the offending axis with a narrower
7104 // diagnostic than the `NomeInvalid` parse arm would emit.
7105 let c = caixa_with_nome("");
7106 let err = c.validate_nome().unwrap_err();
7107 assert_eq!(err, ManifestError::NomeEmpty);
7108 }
7109
7110 #[test]
7111 fn validate_nome_rejects_uppercase() {
7112 // The canonical "I copied the TitleCase display name verbatim"
7113 // footgun. The K8s apiserver rejects `metadata.name: MyApp` at
7114 // admission on every derived artifact (Helm chart, ComputeUnit,
7115 // CNP, HTTPRoute, label values); the gate moves the diagnostic
7116 // to the source `caixa.lisp` and the reason suggests the
7117 // lowercased fix verbatim.
7118 let c = caixa_with_nome("MyApp");
7119 let err = c.validate_nome().unwrap_err();
7120 let ManifestError::NomeInvalid { nome, reason } = err else {
7121 panic!("expected NomeInvalid for uppercase :nome");
7122 };
7123 assert_eq!(nome, "MyApp");
7124 assert!(
7125 reason.contains("uppercase") && reason.contains("myapp"),
7126 "diagnostic must name the violation + the lowercased fix, got {reason:?}"
7127 );
7128 }
7129
7130 #[test]
7131 fn validate_nome_rejects_underscore() {
7132 // The Python-/Postgres-style `snake_case` leak. DNS-1123 forbids
7133 // `_`; the apiserver rejects on admission across every derived
7134 // artifact. Same fixture pinned for `:membros :caixa` (3f9d7a0)
7135 // and `:children :caixa` (31bfa43).
7136 let c = caixa_with_nome("my_app");
7137 let err = c.validate_nome().unwrap_err();
7138 assert!(
7139 matches!(
7140 err,
7141 ManifestError::NomeInvalid { ref nome, ref reason }
7142 if nome == "my_app" && reason.contains('_')
7143 ),
7144 "got {err:?}"
7145 );
7146 }
7147
7148 #[test]
7149 fn validate_nome_rejects_dot() {
7150 // A `:nome` is a single DNS-1123 label, not a subdomain. The
7151 // "I want to namespace with `.`" footgun the gate redirects to
7152 // `-` via the shared predicate's reason wording.
7153 let c = caixa_with_nome("team.app");
7154 let err = c.validate_nome().unwrap_err();
7155 assert!(
7156 matches!(
7157 err,
7158 ManifestError::NomeInvalid { ref nome, ref reason }
7159 if nome == "team.app" && reason.contains('.')
7160 ),
7161 "got {err:?}"
7162 );
7163 }
7164
7165 #[test]
7166 fn validate_nome_rejects_leading_hyphen() {
7167 // DNS-1123 boundary rule: the label must start with an ASCII
7168 // alphanumeric. Pin the leading-`-` arm explicitly.
7169 let c = caixa_with_nome("-app");
7170 let err = c.validate_nome().unwrap_err();
7171 assert!(
7172 matches!(
7173 err,
7174 ManifestError::NomeInvalid { ref nome, .. } if nome == "-app"
7175 ),
7176 "got {err:?}"
7177 );
7178 }
7179
7180 #[test]
7181 fn validate_nome_rejects_trailing_hyphen() {
7182 // Symmetric arm of the boundary rule, pinned separately so a
7183 // future relaxation that only checks the leading position
7184 // surfaces here. Mirrors `rejects_membro_caixa_with_trailing_hyphen`
7185 // and `_with_trailing_hyphen` on the supervisor / aplicacao
7186 // axes.
7187 let c = caixa_with_nome("app-");
7188 let err = c.validate_nome().unwrap_err();
7189 assert!(
7190 matches!(
7191 err,
7192 ManifestError::NomeInvalid { ref nome, .. } if nome == "app-"
7193 ),
7194 "got {err:?}"
7195 );
7196 }
7197
7198 #[test]
7199 fn validate_nome_rejects_unicode() {
7200 // IDN must be pre-encoded as Punycode (`xn--…`); raw Unicode
7201 // bytes are rejected by the K8s apiserver on every name axis.
7202 let c = caixa_with_nome("café");
7203 let err = c.validate_nome().unwrap_err();
7204 assert!(
7205 matches!(
7206 err,
7207 ManifestError::NomeInvalid { ref nome, .. } if nome == "café"
7208 ),
7209 "got {err:?}"
7210 );
7211 }
7212
7213 #[test]
7214 fn validate_nome_rejects_whitespace() {
7215 // The paste-from-sketch / paste-from-spec footgun. Internal
7216 // whitespace is rejected by every K8s name axis.
7217 let c = caixa_with_nome("my app");
7218 let err = c.validate_nome().unwrap_err();
7219 assert!(
7220 matches!(
7221 err,
7222 ManifestError::NomeInvalid { ref nome, .. } if nome == "my app"
7223 ),
7224 "got {err:?}"
7225 );
7226 }
7227
7228 #[test]
7229 fn validate_nome_rejects_too_long() {
7230 // 64-byte boundary pin: the K8s apiserver rejects any
7231 // `metadata.name` over 63 bytes at admission; the diagnostic
7232 // names both the 63-byte cap and the actual length so the
7233 // author can shorten in one edit. Mirrors `_too_long` on the
7234 // peer member-/cluster-/child-name axes.
7235 let over = "a".repeat(crate::DNS_1123_LABEL_MAX_LEN + 1);
7236 let c = caixa_with_nome(&over);
7237 let err = c.validate_nome().unwrap_err();
7238 let ManifestError::NomeInvalid { nome, reason } = err else {
7239 panic!("expected NomeInvalid for over-cap :nome");
7240 };
7241 assert_eq!(nome.len(), crate::DNS_1123_LABEL_MAX_LEN + 1);
7242 assert!(
7243 reason.contains("63") && reason.contains("64"),
7244 "diagnostic must name the cap + actual length, got {reason:?}"
7245 );
7246 }
7247
7248 #[test]
7249 fn nome_max_length_validates() {
7250 // The 63-byte cap exactly — the boundary-accepting case pinned
7251 // alongside `validate_nome_rejects_too_long` so a future cap
7252 // shift surfaces both arms simultaneously. Mirrors
7253 // `membro_caixa_max_length_validates`,
7254 // `placement_cluster_max_length_validates`,
7255 // `child_caixa_max_length_validates`.
7256 let at_cap = "a".repeat(crate::DNS_1123_LABEL_MAX_LEN);
7257 caixa_with_nome(&at_cap).validate_nome().unwrap();
7258 }
7259
7260 #[test]
7261 fn nome_empty_takes_precedence_over_invalid() {
7262 // Order pin: the empty arm fires before the predicate is
7263 // consulted. Empty < invalid in self-locating-ness — the
7264 // narrower `NomeEmpty` diagnostic doesn't carry a useless
7265 // `nome: ""` reference into the parser-shaped reason. Mirrors
7266 // `membro_caixa_empty_takes_precedence_over_invalid` on the
7267 // peer axis (3f9d7a0).
7268 let c = caixa_with_nome("");
7269 assert_eq!(c.validate_nome().unwrap_err(), ManifestError::NomeEmpty);
7270 }
7271
7272 #[test]
7273 fn nome_invalid_diagnostic_carries_offending_nome() {
7274 // Diagnostic-shape pin: the error names the offending `:nome`
7275 // verbatim with a non-empty parser-shaped reason, so a `feira
7276 // lint` run can render the diagnostic without re-parsing.
7277 // Mirrors `membro_caixa_invalid_diagnostic_carries_offending_caixa`.
7278 let c = caixa_with_nome("MyApp");
7279 let err = c.validate_nome().unwrap_err();
7280 let ManifestError::NomeInvalid { nome, reason } = err else {
7281 panic!("expected NomeInvalid variant");
7282 };
7283 assert_eq!(nome, "MyApp");
7284 assert!(
7285 !reason.is_empty(),
7286 "NomeInvalid `reason` must carry the predicate's wording verbatim"
7287 );
7288 }
7289
7290 // ── Caixa::validate_nome_chart_name_budget — joint-length on `:nome` ──
7291 //
7292 // The bare-`:nome` axis [`Caixa::validate_nome`] caps at 63 bytes
7293 // via DNS-1123; this second-axis gate caps the joint
7294 // `lareira-<nome>` chart name at the same 63-byte ceiling. The
7295 // canonical [`crate::lareira_chart_name`] helper's doc comment
7296 // (f7320d7, caixa-core/src/render.rs:3198) explicitly deferred:
7297 // "the M4 admission webhook will pin the joint-length invariant
7298 // when it lands". These tests pin it at the manifest-validate
7299 // layer instead, fail-before-pass-after on the 56-byte boundary.
7300
7301 #[test]
7302 fn validate_nome_chart_name_budget_accepts_canonical_template() {
7303 // Positive control: the bare `feira init`-style template's
7304 // `:nome` ("demo") sits far below the cap; the gate must not
7305 // regress this baseline. Same shape every peer
7306 // value-shape-gate baseline pin uses.
7307 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
7308 c.validate_nome_chart_name_budget().unwrap();
7309 }
7310
7311 #[test]
7312 fn validate_nome_chart_name_budget_accepts_canonical_fixtures() {
7313 // Positive-set sweep across the canonical author surface every
7314 // in-tree fixture uses (`hello-rio`, `cart`, `checkout`,
7315 // `worker`, the `checkout-aplicacao` example members, the
7316 // `akeyless-attest` caixa-tatara fixture). Every value sits
7317 // far below the 55-byte per-`:nome` budget. Same shape every
7318 // peer per-axis baseline pin uses.
7319 for nome in [
7320 "hello-rio",
7321 "cart",
7322 "checkout",
7323 "worker",
7324 "akeyless-attest",
7325 "demo",
7326 "a",
7327 ] {
7328 caixa_with_nome(nome)
7329 .validate_nome_chart_name_budget()
7330 .unwrap_or_else(|e| {
7331 panic!("canonical :nome {nome:?} must pass chart-name budget, got {e:?}")
7332 });
7333 }
7334 }
7335
7336 #[test]
7337 fn validate_nome_chart_name_budget_accepts_nome_at_cap() {
7338 // Boundary-accepting case at the 55-byte per-`:nome` budget —
7339 // the joint chart name is exactly 63 bytes, the DNS-1123 label
7340 // cap. Pinned alongside the rejecting-arm test so a future cap
7341 // shift surfaces both arms simultaneously. Mirrors
7342 // `nome_max_length_validates` on the peer bare-`:nome` axis.
7343 let at_cap = "a".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN);
7344 caixa_with_nome(&at_cap)
7345 .validate_nome_chart_name_budget()
7346 .unwrap();
7347 }
7348
7349 #[test]
7350 fn validate_nome_chart_name_budget_rejects_nome_one_over_cap() {
7351 // Fail-before-pass-after pin on the 56-byte boundary: the
7352 // smallest `:nome` length that overflows the joint chart-name
7353 // cap. The inner [`is_dns_1123_label`] gate
7354 // (`Caixa::validate_nome`) accepts it (56 ≤ 63), so prior to
7355 // this gate it silently passed the manifest-validate cascade
7356 // and surfaced as a `helm lint` / apiserver rejection on the
7357 // rendered chart name far from the source `caixa.lisp`, with
7358 // no field naming the overflow. With this gate the diagnostic
7359 // names the offending `:nome` verbatim alongside the rendered
7360 // chart name and the budget, so the author can shorten in one
7361 // edit. Mirrors `validate_nome_rejects_too_long` on the peer
7362 // bare-`:nome` axis.
7363 let over = "a".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 1);
7364 let c = caixa_with_nome(&over);
7365 let err = c.validate_nome_chart_name_budget().unwrap_err();
7366 let ManifestError::NomeChartNameBudgetExceeded { nome, reason } = err else {
7367 panic!("expected NomeChartNameBudgetExceeded for over-budget :nome");
7368 };
7369 assert_eq!(nome.len(), crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 1);
7370 assert_eq!(nome, over);
7371 assert!(
7372 reason.contains("63") && reason.contains("64") && reason.contains("55"),
7373 "diagnostic must name the DNS-1123 cap (63), the actual chart-name length (64), \
7374 and the per-`:nome` budget (55), got {reason:?}"
7375 );
7376 }
7377
7378 #[test]
7379 fn validate_nome_chart_name_budget_rejects_nome_at_bare_dns_cap() {
7380 // The 63-byte `:nome` boundary — passes the bare-`:nome`
7381 // [`is_dns_1123_label`] cap exactly, but produces a 71-byte
7382 // joint chart name that overflows the DNS-1123 label cap
7383 // structurally. The most stringent fail-before-pass-after
7384 // surface: every `:nome` in the 56..=63-byte range passed the
7385 // prior cascade and broke at admission.
7386 let bare_max = "a".repeat(crate::DNS_1123_LABEL_MAX_LEN);
7387 let c = caixa_with_nome(&bare_max);
7388 // The bare-`:nome` gate accepts the 63-byte length.
7389 c.validate_nome().unwrap();
7390 // The new joint-length gate rejects it.
7391 let err = c.validate_nome_chart_name_budget().unwrap_err();
7392 assert!(
7393 matches!(
7394 err,
7395 ManifestError::NomeChartNameBudgetExceeded { ref nome, .. }
7396 if nome.len() == crate::DNS_1123_LABEL_MAX_LEN
7397 ),
7398 "got {err:?}"
7399 );
7400 }
7401
7402 #[test]
7403 fn validate_nome_chart_name_budget_diagnostic_carries_offending_chart_name() {
7404 // Diagnostic-shape pin: the rendered `lareira-<nome>` chart
7405 // name appears verbatim in the diagnostic so the author sees
7406 // exactly the string the apiserver / `helm lint` would have
7407 // rejected — no re-derivation required to grep the source.
7408 // Peer with `nome_invalid_diagnostic_carries_offending_nome`
7409 // on the bare-`:nome` axis.
7410 let over = "x".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 5);
7411 let c = caixa_with_nome(&over);
7412 let err = c.validate_nome_chart_name_budget().unwrap_err();
7413 let ManifestError::NomeChartNameBudgetExceeded { nome, reason } = err else {
7414 panic!("expected NomeChartNameBudgetExceeded variant");
7415 };
7416 assert_eq!(nome, over);
7417 let expected_chart = crate::lareira_chart_name(&over);
7418 assert!(
7419 reason.contains(&expected_chart),
7420 "diagnostic must carry the rendered chart name {expected_chart:?} verbatim, \
7421 got {reason:?}"
7422 );
7423 assert!(
7424 reason.contains("lareira-"),
7425 "diagnostic must name the canonical chart-name prefix verbatim, got {reason:?}"
7426 );
7427 }
7428
7429 #[test]
7430 fn validate_nome_chart_name_budget_runs_after_nome_shape_via_layout_verify() {
7431 // Order pin on the layout cascade: the narrower
7432 // `NomeInvalid` (bare-DNS-1123 shape) fires before the
7433 // joint-length budget. A structurally-malformed `:nome` (here:
7434 // uppercase) surfaces its specific shape error rather than
7435 // the chart-name-budget error, even when the joint length
7436 // would also overflow — the narrower diagnostic is more
7437 // self-locating. Mirrors the cascade-precedence pins peer
7438 // gates already use (e.g. `EntradaParaEmpty` before
7439 // `EntradaParaInvalid`).
7440 let over = "A".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 1);
7441 let c = caixa_with_nome(&over);
7442 // The bare-shape gate fires first.
7443 let err = c.validate_nome().unwrap_err();
7444 assert!(
7445 matches!(err, ManifestError::NomeInvalid { .. }),
7446 "bare-shape gate must fire before chart-name-budget gate; got {err:?}"
7447 );
7448 // And the layout verify cascade surfaces that diagnostic, not
7449 // the budget arm. Inject a path-exists oracle so the cascade
7450 // gets past the manifest-presence check and into the
7451 // value-shape gates.
7452 let layout = crate::StandardLayout::new().with_path_exists(|_| true);
7453 let err = crate::LayoutInvariants::verify(
7454 &layout,
7455 &c,
7456 std::path::Path::new("/tmp/caixa-test-fake-root"),
7457 )
7458 .unwrap_err();
7459 let issue = err.to_string();
7460 assert!(
7461 issue.contains("DNS-1123") || issue.contains("uppercase"),
7462 "layout cascade must surface the bare-DNS-1123 diagnostic on a \
7463 structurally-malformed :nome, not the chart-name-budget diagnostic; got {issue:?}"
7464 );
7465 }
7466
7467 #[test]
7468 fn layout_verify_routes_chart_name_budget_through_nome_violation() {
7469 // Cross-axis envelope pin: the layout cascade wraps both
7470 // bare-`:nome` and joint-length-`:nome` failures through the
7471 // same [`LayoutError::NomeViolation`] envelope, since both
7472 // arms are on the `:nome` axis. The user's diagnostic stays
7473 // self-locating ("which axis"), and a future consumer that
7474 // dispatches on the layout-error variant (e.g. a `feira lint`
7475 // exit-code mapping) sees a single per-axis envelope. The
7476 // wrapped `issue:` carries the full inner diagnostic.
7477 let over = "a".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 1);
7478 let c = caixa_with_nome(&over);
7479 // The bare-shape gate accepts.
7480 c.validate_nome().unwrap();
7481 let layout = crate::StandardLayout::new().with_path_exists(|_| true);
7482 let err = crate::LayoutInvariants::verify(
7483 &layout,
7484 &c,
7485 std::path::Path::new("/tmp/caixa-test-fake-root"),
7486 )
7487 .unwrap_err();
7488 let crate::LayoutError::NomeViolation { caixa, issue } = err else {
7489 panic!("expected LayoutError::NomeViolation, got {err:?}");
7490 };
7491 assert_eq!(caixa, over);
7492 assert!(
7493 issue.contains("lareira-") && issue.contains("63") && issue.contains("55"),
7494 "wrapped issue must carry the joint-length diagnostic verbatim, got {issue:?}"
7495 );
7496 }
7497
7498 // ── Caixa::validate_versao — top-level :versao value-shape gate ─────
7499
7500 fn caixa_with_versao(versao: &str) -> Caixa {
7501 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
7502 c.versao = versao.to_string();
7503 c
7504 }
7505
7506 #[test]
7507 fn validate_versao_accepts_canonical_template() {
7508 // Positive control: the bare `feira init`-style template's
7509 // `:versao` ("0.1.0") is a canonical SemVer-2 literal; the gate
7510 // must not regress this baseline shape. A future tightening of
7511 // the accepted set surfaces here as a test failure first.
7512 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
7513 c.validate_versao().unwrap();
7514 }
7515
7516 #[test]
7517 fn validate_versao_accepts_canonical_forms() {
7518 // Positive-set sweep: each realistic SemVer-2 shape the
7519 // substrate's downstream consumers accept must pass — bare
7520 // MAJOR.MINOR.PATCH, pre-release tags (`-rc.1`, `-alpha.0`),
7521 // build metadata (`+build.42`), the combined form, and the
7522 // `0.0.0` boundary case. Mirrors `accepts_canonical_forms` on
7523 // the peer `:nome` axis (6c992f8).
7524 for versao in [
7525 "0.1.0",
7526 "0.0.0",
7527 "1.0.0",
7528 "0.2.0-rc.1",
7529 "1.0.0-alpha.0",
7530 "1.0.0+build.42",
7531 "1.0.0-rc.1+build.42",
7532 "10.20.30",
7533 ] {
7534 caixa_with_versao(versao)
7535 .validate_versao()
7536 .unwrap_or_else(|e| {
7537 panic!("canonical :versao {versao:?} must validate, got {e:?}")
7538 });
7539 }
7540 }
7541
7542 #[test]
7543 fn validate_versao_rejects_empty() {
7544 // Fail-before-pass-after pin: `Caixa::from_lisp` does not refuse
7545 // an empty `:versao` (the derive macro stores the raw String);
7546 // the gate's empty arm names the offending axis with a narrower
7547 // diagnostic than the `VersaoInvalid` parse arm would emit.
7548 // Mirrors `validate_nome_rejects_empty` (6c992f8).
7549 let c = caixa_with_versao("");
7550 let err = c.validate_versao().unwrap_err();
7551 assert_eq!(err, ManifestError::VersaoEmpty);
7552 }
7553
7554 #[test]
7555 fn validate_versao_rejects_git_tag_shape() {
7556 // The canonical "I copied the git tag verbatim" footgun —
7557 // `feira publish` *emits* `v<versao>` git tags, so a leaked
7558 // `v0.1.0` in `:versao` would render as `vv0.1.0` and silently
7559 // shift every downstream consumer's version axis. `semver`
7560 // rejects the leading `v` at parse time; the gate moves the
7561 // diagnostic to the source `caixa.lisp`.
7562 let c = caixa_with_versao("v0.1.0");
7563 let err = c.validate_versao().unwrap_err();
7564 let ManifestError::VersaoInvalid { versao, reason } = err else {
7565 panic!("expected VersaoInvalid for git-tag-shape :versao");
7566 };
7567 assert_eq!(versao, "v0.1.0");
7568 assert!(
7569 !reason.is_empty(),
7570 "VersaoInvalid `reason` must carry the parser's wording, got {reason:?}"
7571 );
7572 }
7573
7574 #[test]
7575 fn validate_versao_rejects_missing_patch() {
7576 // The canonical "I shortened it" footgun — SemVer-2 requires
7577 // three parts. Cargo's `version =` field accepts the shortened
7578 // form as a requirement, conflating the two leaks across the
7579 // typed `:deps :versao` vs top-level `:versao` axes; the gate
7580 // pins the top-level axis to the strict three-part shape.
7581 let c = caixa_with_versao("0.1");
7582 let err = c.validate_versao().unwrap_err();
7583 assert!(
7584 matches!(
7585 err,
7586 ManifestError::VersaoInvalid { ref versao, .. } if versao == "0.1"
7587 ),
7588 "got {err:?}"
7589 );
7590 }
7591
7592 #[test]
7593 fn validate_versao_rejects_requirement_shape() {
7594 // The canonical "I leaked a requirement into a version" footgun —
7595 // the typed `:deps :versao` / `:membros :versao` axes accept
7596 // `^0.1` (a `VersionReq`); the top-level `:versao` requires a
7597 // concrete `Version`. Without this gate the two typed surfaces
7598 // would silently overlap, and a top-level `^0.1` would surface
7599 // at `helm install` time as a Chart.yaml version rejection far
7600 // from the source `caixa.lisp`.
7601 let c = caixa_with_versao("^0.1");
7602 let err = c.validate_versao().unwrap_err();
7603 assert!(
7604 matches!(
7605 err,
7606 ManifestError::VersaoInvalid { ref versao, .. } if versao == "^0.1"
7607 ),
7608 "got {err:?}"
7609 );
7610 }
7611
7612 #[test]
7613 fn validate_versao_rejects_docker_tag_shape() {
7614 // The "I confused it with a docker tag" footgun — `latest`,
7615 // `main`, `stable` parse as identifiers, not SemVer-2 versions.
7616 // SemVer rejects at parse time; the gate moves the diagnostic
7617 // to the source `caixa.lisp`.
7618 for bad in ["latest", "main", "stable"] {
7619 let c = caixa_with_versao(bad);
7620 let err = c.validate_versao().unwrap_err();
7621 assert!(
7622 matches!(
7623 err,
7624 ManifestError::VersaoInvalid { ref versao, .. } if versao == bad
7625 ),
7626 "got {err:?} for {bad:?}"
7627 );
7628 }
7629 }
7630
7631 #[test]
7632 fn validate_versao_rejects_four_part_form() {
7633 // The Java/Microsoft "MAJOR.MINOR.PATCH.BUILD" convention
7634 // SemVer-2 forbids. A leak from a non-SemVer ecosystem; the
7635 // semver crate rejects the extra `.0` at parse time.
7636 let c = caixa_with_versao("0.1.0.0");
7637 let err = c.validate_versao().unwrap_err();
7638 assert!(
7639 matches!(
7640 err,
7641 ManifestError::VersaoInvalid { ref versao, .. } if versao == "0.1.0.0"
7642 ),
7643 "got {err:?}"
7644 );
7645 }
7646
7647 #[test]
7648 fn versao_empty_takes_precedence_over_invalid() {
7649 // Order pin: the empty arm fires before the parser is consulted.
7650 // Empty < invalid in self-locating-ness — the narrower
7651 // `VersaoEmpty` diagnostic doesn't carry a useless `versao: ""`
7652 // reference into the parser-shaped reason. Mirrors
7653 // `nome_empty_takes_precedence_over_invalid` (6c992f8) on the
7654 // peer axis.
7655 let c = caixa_with_versao("");
7656 assert_eq!(c.validate_versao().unwrap_err(), ManifestError::VersaoEmpty);
7657 }
7658
7659 #[test]
7660 fn versao_invalid_diagnostic_carries_offending_versao() {
7661 // Diagnostic-shape pin: the error names the offending `:versao`
7662 // verbatim with a non-empty parser-shaped reason, so a `feira
7663 // lint` run can render the diagnostic without re-parsing.
7664 // Mirrors `nome_invalid_diagnostic_carries_offending_nome`.
7665 let c = caixa_with_versao("v0.1.0");
7666 let err = c.validate_versao().unwrap_err();
7667 let ManifestError::VersaoInvalid { versao, reason } = err else {
7668 panic!("expected VersaoInvalid variant");
7669 };
7670 assert_eq!(versao, "v0.1.0");
7671 assert!(
7672 !reason.is_empty(),
7673 "VersaoInvalid `reason` must carry the parser's wording verbatim"
7674 );
7675 }
7676
7677 #[test]
7678 fn validate_versao_accepts_what_upgrade_from_from_accepts() {
7679 // Parity pin: every shape `UpgradeFromEntry::validate` accepts
7680 // for `:upgrade-from :from` must also pass `validate_versao` —
7681 // the two `:versao`-typed surfaces (top-level `:versao`,
7682 // `:upgrade-from :from`) consume the *same* `semver::Version`
7683 // parser, so they must agree on the accepted set. Without this
7684 // pin, a future tightening of one axis could silently diverge
7685 // from the other. Mirrors the `:versao` requirement-axis
7686 // parity (`:deps`/`:deps-dev`/`:membros`/`:children`) the prior
7687 // commits established.
7688 for versao in ["0.1.0", "0.2.0-rc.1", "1.0.0+build.42"] {
7689 // From the canonical UpgradeFromEntry round-trip fixture
7690 // (`upgrade::tests::round_trip_load_module` peers).
7691 let entry = crate::UpgradeFromEntry {
7692 from: versao.to_string(),
7693 instructions: Vec::new(),
7694 };
7695 entry
7696 .validate()
7697 .unwrap_or_else(|e| panic!(":from {versao:?} must validate, got {e:?}"));
7698 caixa_with_versao(versao)
7699 .validate_versao()
7700 .unwrap_or_else(|e| {
7701 panic!(":versao {versao:?} must validate, got {e:?} — peer axis diverges")
7702 });
7703 }
7704 }
7705
7706 // ── Caixa::validate_restart_window — supervisor restart-window
7707 // folds through the shared `supervisor::duration_codec` ────────
7708
7709 fn caixa_with_restart_window(window: Option<&str>) -> Caixa {
7710 let mut c = Caixa::from_lisp(&Caixa::template("root")).unwrap();
7711 c.kind = CaixaKind::Supervisor;
7712 c.restart_window = window.map(str::to_string);
7713 c
7714 }
7715
7716 #[test]
7717 fn validate_restart_window_accepts_none() {
7718 // The canonical "omit the slot to express no reset" shape — a
7719 // `None` raw string is the absence of the typed
7720 // `:restart-window` slot, which is exactly the SupervisorSpec
7721 // "never reset" semantics. The gate must be a no-op here; a
7722 // future tightening that rejected `None` would force every
7723 // supervisor caixa to authoring-time pin a window even when
7724 // the OTP semantics call for none.
7725 caixa_with_restart_window(None)
7726 .validate_restart_window()
7727 .unwrap();
7728 }
7729
7730 #[test]
7731 fn validate_restart_window_accepts_canonical_forms() {
7732 // Positive-set sweep across the canonical authoring units the
7733 // shared `supervisor::duration_codec::parse` accepts —
7734 // matches the codec-side `parse_accepts_integer_canonical_units`
7735 // pin in supervisor::tests so a future codec-side tightening
7736 // surfaces simultaneously on both axes.
7737 for window in ["60s", "5m", "1h", "500ms", "30", "0s"] {
7738 caixa_with_restart_window(Some(window))
7739 .validate_restart_window()
7740 .unwrap_or_else(|e| {
7741 panic!("canonical :restart-window {window:?} must validate, got {e:?}")
7742 });
7743 }
7744 }
7745
7746 #[test]
7747 fn validate_restart_window_rejects_fractional_seconds() {
7748 // Fail-before-pass-after pin: the `"1.5s"` drift class (parses
7749 // as f64 to 1.5 → renders back as `"1500ms"` on first
7750 // serialize). Prior to the fold + this gate, the inline
7751 // `parse_window_inline` accepted f64 magnitudes and silently
7752 // produced a `Duration::from_secs_f64(1.5)`, divergent from
7753 // the shared codec's integer-magnitude discipline on the
7754 // serde-routed siblings. The gate now surfaces a self-locating
7755 // diagnostic at the manifest layer.
7756 let err = caixa_with_restart_window(Some("1.5s"))
7757 .validate_restart_window()
7758 .unwrap_err();
7759 let ManifestError::RestartWindowMalformed {
7760 restart_window,
7761 reason,
7762 } = err
7763 else {
7764 panic!("expected RestartWindowMalformed for fractional seconds");
7765 };
7766 assert_eq!(restart_window, "1.5s");
7767 assert!(
7768 reason.contains("\"1.5\"") && reason.contains("not a non-negative integer"),
7769 "diagnostic must carry shared-codec wording, got {reason:?}"
7770 );
7771 }
7772
7773 #[test]
7774 fn validate_restart_window_rejects_decimal_shaped_integer() {
7775 // The `"1.0s"` class — numerically `1s` exactly, but the
7776 // canonical form is `"1s"` not `"1.0s"`. Decimal-shape leak
7777 // gets the same canonical-form diagnostic.
7778 let err = caixa_with_restart_window(Some("1.0s"))
7779 .validate_restart_window()
7780 .unwrap_err();
7781 assert!(
7782 matches!(
7783 err,
7784 ManifestError::RestartWindowMalformed { ref restart_window, .. }
7785 if restart_window == "1.0s"
7786 ),
7787 "got {err:?}"
7788 );
7789 }
7790
7791 #[test]
7792 fn validate_restart_window_rejects_half_unit_minute() {
7793 // `"0.5m"` is the unit-fraction footgun — author writes a
7794 // human-readable half-minute, the prior inline parser silently
7795 // produced `Duration::from_secs_f64(30.0)` and serde
7796 // re-emitted as `"30s"`, rewriting author intent. The gate
7797 // closes the loop at the manifest layer.
7798 let err = caixa_with_restart_window(Some("0.5m"))
7799 .validate_restart_window()
7800 .unwrap_err();
7801 let ManifestError::RestartWindowMalformed {
7802 restart_window,
7803 reason,
7804 } = err
7805 else {
7806 panic!("expected RestartWindowMalformed");
7807 };
7808 assert_eq!(restart_window, "0.5m");
7809 assert!(
7810 reason.contains("\"30s\""),
7811 "diagnostic must point at the canonical-form remediation, got {reason:?}"
7812 );
7813 }
7814
7815 #[test]
7816 fn validate_restart_window_rejects_leading_sign() {
7817 // `"+30s"` and `"-30s"` both round-tripped through f64 cleanly
7818 // on the prior parser (`+30` parses as `30.0`; `-30` parsed
7819 // and was caught by the `num < 0.0` arm which silently
7820 // returned `None`, dropping the author-supplied window). The
7821 // shared codec's digit-only gate rejects both with a unified
7822 // canonical-form diagnostic; the manifest-layer wrapper names
7823 // the offending value.
7824 for bad in ["+30s", "-30s"] {
7825 let err = caixa_with_restart_window(Some(bad))
7826 .validate_restart_window()
7827 .unwrap_err();
7828 assert!(
7829 matches!(
7830 err,
7831 ManifestError::RestartWindowMalformed { ref restart_window, .. }
7832 if restart_window == bad
7833 ),
7834 "got {err:?} for {bad:?}"
7835 );
7836 }
7837 }
7838
7839 #[test]
7840 fn validate_restart_window_rejects_unknown_unit() {
7841 // `"30x"` — the typo / wrong-unit footgun. The shared codec's
7842 // unit dispatch surfaces an `unknown duration unit` reason;
7843 // the manifest-layer wrapper names the offending value.
7844 let err = caixa_with_restart_window(Some("30x"))
7845 .validate_restart_window()
7846 .unwrap_err();
7847 let ManifestError::RestartWindowMalformed {
7848 restart_window,
7849 reason,
7850 } = err
7851 else {
7852 panic!("expected RestartWindowMalformed for unknown unit");
7853 };
7854 assert_eq!(restart_window, "30x");
7855 assert!(
7856 reason.contains("unknown duration unit"),
7857 "diagnostic must carry shared-codec unit-rejection wording, got {reason:?}"
7858 );
7859 }
7860
7861 #[test]
7862 fn validate_restart_window_rejects_garbage() {
7863 // Pure non-numeric magnitude (`"abc"`) falls through to the
7864 // shared codec's narrower `"bad duration magnitude"` arm. Same
7865 // diagnostic shape as the codec-side
7866 // `parse_garbage_still_falls_through_to_bad_magnitude` pin.
7867 let err = caixa_with_restart_window(Some("abc"))
7868 .validate_restart_window()
7869 .unwrap_err();
7870 let ManifestError::RestartWindowMalformed {
7871 restart_window,
7872 reason,
7873 } = err
7874 else {
7875 panic!("expected RestartWindowMalformed for garbage");
7876 };
7877 assert_eq!(restart_window, "abc");
7878 assert!(
7879 reason.contains("bad duration magnitude"),
7880 "diagnostic must carry shared-codec garbage-rejection wording, got {reason:?}"
7881 );
7882 }
7883
7884 #[test]
7885 fn validate_restart_window_rejects_empty_string() {
7886 // The empty-after-trim edge case — distinct from the `None`
7887 // canonical "omit the slot" shape. The shared codec's
7888 // digit-only gate refuses an empty magnitude; the manifest
7889 // layer names the offending `""` so the author can grep for
7890 // the literal empty value in their `caixa.lisp` and either
7891 // remove the slot (the canonical "no reset" shape) or pin a
7892 // positive duration.
7893 let err = caixa_with_restart_window(Some(""))
7894 .validate_restart_window()
7895 .unwrap_err();
7896 assert!(
7897 matches!(
7898 err,
7899 ManifestError::RestartWindowMalformed { ref restart_window, .. }
7900 if restart_window.is_empty()
7901 ),
7902 "got {err:?}"
7903 );
7904 }
7905
7906 #[test]
7907 fn validate_restart_window_diagnostic_carries_offending_value() {
7908 // Diagnostic-shape pin (peer with
7909 // `nome_invalid_diagnostic_carries_offending_nome` /
7910 // `versao_invalid_diagnostic_carries_offending_versao`): the
7911 // error names the offending raw `:restart-window` verbatim
7912 // with a non-empty shared-codec-shaped reason, so a `feira
7913 // lint` run can render the diagnostic without re-parsing.
7914 let err = caixa_with_restart_window(Some("1.5s"))
7915 .validate_restart_window()
7916 .unwrap_err();
7917 let ManifestError::RestartWindowMalformed {
7918 restart_window,
7919 reason,
7920 } = err
7921 else {
7922 panic!("expected RestartWindowMalformed variant");
7923 };
7924 assert_eq!(restart_window, "1.5s");
7925 assert!(
7926 !reason.is_empty(),
7927 "RestartWindowMalformed `reason` must carry the codec's wording verbatim"
7928 );
7929 }
7930
7931 #[test]
7932 fn supervisor_view_folds_through_shared_codec_on_canonical_form() {
7933 // Behavioral parity pin after the fold (`parse_window_inline`
7934 // deletion): the canonical `"60s"` still produces
7935 // `Duration::from_secs(60)` on the typed view — the fold is
7936 // semantically equivalent to the prior inline parser on the
7937 // accepted set. Mirrors the pre-fold `supervisor_view_returns_typed_shape`
7938 // pin, narrowed to the parser-side contract.
7939 let c = caixa_with_restart_window(Some("60s"));
7940 let view = c.supervisor_view().expect("Supervisor kind has a view");
7941 assert_eq!(
7942 view.restart_window,
7943 Some(std::time::Duration::from_secs(60))
7944 );
7945 }
7946
7947 #[test]
7948 fn supervisor_view_soft_swallows_what_validate_rejects() {
7949 // Parity pin between the view-construction path and the
7950 // manifest-level validator: the same `"1.5s"` that surfaces
7951 // `RestartWindowMalformed` at `validate_restart_window` time
7952 // becomes `restart_window: None` on the typed view (the fold
7953 // preserves the existing best-effort shape of `supervisor_view`).
7954 // The contract is: a layout-verifier / `feira lint` flow that
7955 // cares about the malformed-window axis MUST consult
7956 // `validate_restart_window` — relying solely on the view's
7957 // `None` swallows the diagnostic silently. This pin makes the
7958 // expectation a typed invariant.
7959 let c = caixa_with_restart_window(Some("1.5s"));
7960 let view = c.supervisor_view().expect("Supervisor kind has a view");
7961 assert_eq!(
7962 view.restart_window, None,
7963 "view-construction path soft-swallows the parse error to None"
7964 );
7965 // And the manifest-level validator does NOT soft-swallow:
7966 assert!(
7967 matches!(
7968 c.validate_restart_window().unwrap_err(),
7969 ManifestError::RestartWindowMalformed { ref restart_window, .. }
7970 if restart_window == "1.5s"
7971 ),
7972 "validator must surface the offending value",
7973 );
7974 }
7975
7976 // ── validate_code_paths — per-entry shape on :bibliotecas / :exe / :servicos ──
7977
7978 fn caixa_with_code_paths(bibliotecas: Vec<&str>, exe: Vec<&str>, servicos: Vec<&str>) -> Caixa {
7979 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
7980 c.bibliotecas = bibliotecas.into_iter().map(String::from).collect();
7981 c.exe = exe.into_iter().map(String::from).collect();
7982 c.servicos = servicos.into_iter().map(String::from).collect();
7983 c
7984 }
7985
7986 #[test]
7987 fn validate_code_paths_accepts_canonical_template() {
7988 // The bare `Caixa::template` shape is the gate's identity element
7989 // on the canonical authoring shape — `:bibliotecas
7990 // ("lib/demo.lisp")` + empty `:exe` + empty `:servicos`. Pins
7991 // that the gate is non-disruptive against every existing caixa.
7992 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
7993 c.validate_code_paths().unwrap();
7994 }
7995
7996 #[test]
7997 fn validate_code_paths_accepts_explicit_relative_paths_on_every_slot() {
7998 // Positive control sweep: a canonical-shaped path on every slot
7999 // passes. Mirrors the peer
8000 // `behavior::validate_every_slot_relative_is_ok` pin.
8001 let c = caixa_with_code_paths(
8002 vec!["lib/demo.lisp", "lib/helpers.lisp"],
8003 vec!["exe/demo", "exe/tool"],
8004 vec!["servicos/demo.computeunit.yaml"],
8005 );
8006 c.validate_code_paths().unwrap();
8007 }
8008
8009 #[test]
8010 fn validate_code_paths_accepts_all_empty_lists() {
8011 // The empty-list identity element: every Caixa with no declared
8012 // code paths trivially passes (Supervisor / Aplicacao kinds rely
8013 // on this — the OwnCode gate already rejected them before the
8014 // path-shape gate runs in the layout, but the validator itself
8015 // must accept the empty shape).
8016 let c = caixa_with_code_paths(vec![], vec![], vec![]);
8017 c.validate_code_paths().unwrap();
8018 }
8019
8020 #[test]
8021 fn validate_code_paths_rejects_empty_bibliotecas_entry() {
8022 let c = caixa_with_code_paths(vec![""], vec![], vec![]);
8023 let err = c.validate_code_paths().unwrap_err();
8024 assert!(
8025 matches!(
8026 err,
8027 ManifestError::CodePathEmpty {
8028 slot: ":bibliotecas"
8029 }
8030 ),
8031 "got {err:?}",
8032 );
8033 }
8034
8035 #[test]
8036 fn validate_code_paths_rejects_empty_exe_entry() {
8037 let c = caixa_with_code_paths(vec![], vec![""], vec![]);
8038 let err = c.validate_code_paths().unwrap_err();
8039 assert!(
8040 matches!(err, ManifestError::CodePathEmpty { slot: ":exe" }),
8041 "got {err:?}",
8042 );
8043 }
8044
8045 #[test]
8046 fn validate_code_paths_rejects_empty_servicos_entry() {
8047 let c = caixa_with_code_paths(vec![], vec![], vec![""]);
8048 let err = c.validate_code_paths().unwrap_err();
8049 assert!(
8050 matches!(err, ManifestError::CodePathEmpty { slot: ":servicos" }),
8051 "got {err:?}",
8052 );
8053 }
8054
8055 #[test]
8056 fn validate_code_paths_rejects_absolute_bibliotecas_entry() {
8057 // `:bibliotecas` has no `starts_with(<dir>)` fence downstream,
8058 // so an absolute path that resolves on disk silently passes the
8059 // layout's existence check — the canonical sandbox-escape on
8060 // the biblioteca axis.
8061 let c = caixa_with_code_paths(vec!["/etc/passwd"], vec![], vec![]);
8062 let err = c.validate_code_paths().unwrap_err();
8063 let ManifestError::CodePathAbsolute { slot, path } = err else {
8064 panic!("expected CodePathAbsolute, got {err:?}");
8065 };
8066 assert_eq!(slot, ":bibliotecas");
8067 assert_eq!(path, PathBuf::from("/etc/passwd"));
8068 }
8069
8070 #[test]
8071 fn validate_code_paths_rejects_absolute_exe_entry() {
8072 let c = caixa_with_code_paths(vec![], vec!["/usr/bin/env"], vec![]);
8073 let err = c.validate_code_paths().unwrap_err();
8074 let ManifestError::CodePathAbsolute { slot, path } = err else {
8075 panic!("expected CodePathAbsolute, got {err:?}");
8076 };
8077 assert_eq!(slot, ":exe");
8078 assert_eq!(path, PathBuf::from("/usr/bin/env"));
8079 }
8080
8081 #[test]
8082 fn validate_code_paths_rejects_absolute_servicos_entry() {
8083 let c = caixa_with_code_paths(vec![], vec![], vec!["/var/servicos/x.yaml"]);
8084 let err = c.validate_code_paths().unwrap_err();
8085 let ManifestError::CodePathAbsolute { slot, path } = err else {
8086 panic!("expected CodePathAbsolute, got {err:?}");
8087 };
8088 assert_eq!(slot, ":servicos");
8089 assert_eq!(path, PathBuf::from("/var/servicos/x.yaml"));
8090 }
8091
8092 #[test]
8093 fn validate_code_paths_rejects_parent_escape_bibliotecas_leading() {
8094 // Canonical "I want a lib from a sibling caixa" footgun on the
8095 // biblioteca axis. `:bibliotecas` has no `starts_with` fence
8096 // downstream, so a leading `..` traverses to the parent of the
8097 // caixa root with no diagnostic at layout time if the resolved
8098 // target exists.
8099 let c = caixa_with_code_paths(vec!["../sibling/x.lisp"], vec![], vec![]);
8100 let err = c.validate_code_paths().unwrap_err();
8101 let ManifestError::CodePathParentEscape { slot, path } = err else {
8102 panic!("expected CodePathParentEscape, got {err:?}");
8103 };
8104 assert_eq!(slot, ":bibliotecas");
8105 assert_eq!(path, PathBuf::from("../sibling/x.lisp"));
8106 }
8107
8108 #[test]
8109 fn validate_code_paths_rejects_parent_escape_exe_mid_path() {
8110 // Mid-path `..` defeats the layout's component-aware
8111 // `starts_with(exe_dir)` fence — `root.join("exe/../../escape")`
8112 // `starts_with(<root>/exe)` is true, but the canonical resolution
8113 // lives outside the caixa root. Caught regardless of where the
8114 // `..` sits — mirrors the peer
8115 // `behavior::validate_rejects_parent_escape_mid_path` pin.
8116 let c = caixa_with_code_paths(vec![], vec!["exe/../../escape"], vec![]);
8117 let err = c.validate_code_paths().unwrap_err();
8118 let ManifestError::CodePathParentEscape { slot, path } = err else {
8119 panic!("expected CodePathParentEscape, got {err:?}");
8120 };
8121 assert_eq!(slot, ":exe");
8122 assert_eq!(path, PathBuf::from("exe/../../escape"));
8123 }
8124
8125 #[test]
8126 fn validate_code_paths_rejects_parent_escape_servicos_trailing() {
8127 let c = caixa_with_code_paths(vec![], vec![], vec!["servicos/foo/../../escape.yaml"]);
8128 let err = c.validate_code_paths().unwrap_err();
8129 let ManifestError::CodePathParentEscape { slot, path } = err else {
8130 panic!("expected CodePathParentEscape, got {err:?}");
8131 };
8132 assert_eq!(slot, ":servicos");
8133 assert_eq!(path, PathBuf::from("servicos/foo/../../escape.yaml"));
8134 }
8135
8136 #[test]
8137 fn validate_code_paths_cross_slot_precedence_bibliotecas_before_exe_before_servicos() {
8138 // Cross-slot precedence pin: `:bibliotecas` → `:exe` →
8139 // `:servicos`. A manifest with malformed entries on all three
8140 // surfaces surfaces the `:bibliotecas` defect first, mirroring
8141 // the canonical declaration order
8142 // `Caixa::declared_foreign_code_slots` already establishes for
8143 // the foreign-code-slot diagnostic.
8144 let c = caixa_with_code_paths(vec![""], vec![""], vec![""]);
8145 let err = c.validate_code_paths().unwrap_err();
8146 assert!(
8147 matches!(
8148 err,
8149 ManifestError::CodePathEmpty {
8150 slot: ":bibliotecas"
8151 }
8152 ),
8153 "got {err:?}",
8154 );
8155 }
8156
8157 #[test]
8158 fn validate_code_paths_within_slot_precedence_empty_before_absolute_before_parent_escape() {
8159 // Within-slot precedence pin: empty → absolute → parent-escape,
8160 // matching the [`PathShapeViolation`] arm-ordering every peer
8161 // `is_sandboxed_relative_path` caller follows (b0c8389
8162 // BehaviorSpec, 26da2c7 UpgradeInstruction::StateChange). A
8163 // `:bibliotecas` list whose first entry is empty *and* whose
8164 // later entries are absolute/parent-escape surfaces the empty
8165 // arm first, on the lexicographically-earliest offending entry.
8166 let c = caixa_with_code_paths(vec!["", "/etc/passwd", "../escape.lisp"], vec![], vec![]);
8167 let err = c.validate_code_paths().unwrap_err();
8168 assert!(
8169 matches!(
8170 err,
8171 ManifestError::CodePathEmpty {
8172 slot: ":bibliotecas"
8173 }
8174 ),
8175 "got {err:?}",
8176 );
8177 }
8178
8179 #[test]
8180 fn validate_code_paths_first_offender_per_slot_wins() {
8181 // Within a single slot, the first declaration-order offender
8182 // surfaces — pins that the gate is left-to-right deterministic
8183 // (peer of every `*_first_collision_*` pin on duplicate gates).
8184 let c = caixa_with_code_paths(
8185 vec!["lib/ok.lisp", "/etc/escape", "../also-escape"],
8186 vec![],
8187 vec![],
8188 );
8189 let err = c.validate_code_paths().unwrap_err();
8190 let ManifestError::CodePathAbsolute { slot, path } = err else {
8191 panic!("expected CodePathAbsolute, got {err:?}");
8192 };
8193 assert_eq!(slot, ":bibliotecas");
8194 assert_eq!(path, PathBuf::from("/etc/escape"));
8195 }
8196
8197 #[test]
8198 fn validate_code_paths_diagnostic_carries_offending_slot_and_path() {
8199 // Diagnostic-shape pin (peer with
8200 // `nome_invalid_diagnostic_carries_offending_nome` /
8201 // `versao_invalid_diagnostic_carries_offending_versao`): the
8202 // error's Display surfaces both the offending `:slot` tag and
8203 // the offending path verbatim, so a `feira lint` run can render
8204 // the diagnostic without re-parsing.
8205 let c = caixa_with_code_paths(vec!["/etc/passwd"], vec![], vec![]);
8206 let rendered = c.validate_code_paths().unwrap_err().to_string();
8207 assert!(
8208 rendered.contains(":bibliotecas"),
8209 "diagnostic must name the offending slot: {rendered}",
8210 );
8211 assert!(
8212 rendered.contains("/etc/passwd"),
8213 "diagnostic must quote the offending path: {rendered}",
8214 );
8215 }
8216
8217 #[test]
8218 fn validate_code_paths_rejects_duplicate_bibliotecas_entry() {
8219 // Canonical copy-paste-the-wrong-file footgun on the biblioteca
8220 // axis. Without the gate `feira build` re-parses the same lib
8221 // twice, wasting work and silently masking the author's intent
8222 // to declare a *second* biblioteca.
8223 let c = caixa_with_code_paths(vec!["lib/demo.lisp", "lib/demo.lisp"], vec![], vec![]);
8224 let err = c.validate_code_paths().unwrap_err();
8225 let ManifestError::CodePathDuplicate { slot, path } = err else {
8226 panic!("expected CodePathDuplicate, got {err:?}");
8227 };
8228 assert_eq!(slot, ":bibliotecas");
8229 assert_eq!(path, PathBuf::from("lib/demo.lisp"));
8230 }
8231
8232 #[test]
8233 fn validate_code_paths_rejects_duplicate_exe_entry() {
8234 // Same footgun on the Binario surface. The future `caixa-flake`
8235 // emitter that materializes each `:exe` entry as a flake
8236 // `packages.<name>` derivation would collide on the duplicate
8237 // package key — surfaced here at the typed-validate layer with a
8238 // self-locating diagnostic instead.
8239 let c = caixa_with_code_paths(vec![], vec!["exe/cli", "exe/cli"], vec![]);
8240 let err = c.validate_code_paths().unwrap_err();
8241 let ManifestError::CodePathDuplicate { slot, path } = err else {
8242 panic!("expected CodePathDuplicate, got {err:?}");
8243 };
8244 assert_eq!(slot, ":exe");
8245 assert_eq!(path, PathBuf::from("exe/cli"));
8246 }
8247
8248 #[test]
8249 fn validate_code_paths_rejects_duplicate_servicos_entry() {
8250 // Same footgun on the Servico surface. The peer caixa-helm /
8251 // caixa-flux renderers refuse `:servicos.len() != 1` with the
8252 // narrower `UnsupportedServicoCount` diagnostic, but that
8253 // diagnostic surfaces "too many servicos" without naming
8254 // "duplicate entry" — the typed self-locating framing only lands
8255 // at this gate.
8256 let c = caixa_with_code_paths(
8257 vec![],
8258 vec![],
8259 vec![
8260 "servicos/demo.computeunit.yaml",
8261 "servicos/demo.computeunit.yaml",
8262 ],
8263 );
8264 let err = c.validate_code_paths().unwrap_err();
8265 let ManifestError::CodePathDuplicate { slot, path } = err else {
8266 panic!("expected CodePathDuplicate, got {err:?}");
8267 };
8268 assert_eq!(slot, ":servicos");
8269 assert_eq!(path, PathBuf::from("servicos/demo.computeunit.yaml"));
8270 }
8271
8272 #[test]
8273 fn validate_code_paths_accepts_same_path_across_slots() {
8274 // Per-list scope pin: a `:bibliotecas` entry that happens to
8275 // collide with an `:exe` or `:servicos` entry as a *string* is
8276 // not a duplicate by this gate (each list gets its own HashSet),
8277 // mirroring the peer `:deps` ↔ `:deps-dev` per-list scope
8278 // (a `:nome` present in both lists is a legitimate dev-vs-runtime
8279 // shape on the dep axis). The structural `starts_with(<exe |
8280 // servicos>_dir)` fence at layout time prevents the realistic
8281 // cross-slot collision case from existing on disk, but the gate's
8282 // per-list scope is correct independent of that downstream fence.
8283 let c = caixa_with_code_paths(
8284 vec!["lib/x.lisp"],
8285 vec!["exe/x"],
8286 vec!["servicos/x.computeunit.yaml"],
8287 );
8288 c.validate_code_paths().unwrap();
8289 }
8290
8291 #[test]
8292 fn validate_code_paths_duplicate_fires_after_structural_checks_on_same_slot() {
8293 // Within-slot ordering pin: structural defects (empty / absolute
8294 // / parent-escape) fire before the duplicate gate on the same
8295 // slot. A `:bibliotecas ("" "lib/x.lisp" "lib/x.lisp")` shape
8296 // surfaces the narrower `CodePathEmpty` for the empty entry
8297 // first, not the duplicate on the later pair — same arm-ordering
8298 // every peer per-list duplicate gate uses (`:etiquetas` 360a499,
8299 // `:autores` 86c769b, `:deps` 359fba5).
8300 let c = caixa_with_code_paths(vec!["", "lib/x.lisp", "lib/x.lisp"], vec![], vec![]);
8301 let err = c.validate_code_paths().unwrap_err();
8302 assert!(
8303 matches!(
8304 err,
8305 ManifestError::CodePathEmpty {
8306 slot: ":bibliotecas"
8307 }
8308 ),
8309 "got {err:?}",
8310 );
8311 }
8312
8313 #[test]
8314 fn validate_code_paths_duplicate_in_bibliotecas_fires_before_duplicate_in_exe() {
8315 // Cross-slot ordering pin on the duplicate arm: `:bibliotecas`
8316 // duplicates surface before `:exe` duplicates, matching the
8317 // canonical `:bibliotecas` → `:exe` → `:servicos` declaration
8318 // order every peer per-slot diagnostic on this surface follows.
8319 let c = caixa_with_code_paths(
8320 vec!["lib/x.lisp", "lib/x.lisp"],
8321 vec!["exe/y", "exe/y"],
8322 vec![],
8323 );
8324 let err = c.validate_code_paths().unwrap_err();
8325 let ManifestError::CodePathDuplicate { slot, path } = err else {
8326 panic!("expected CodePathDuplicate, got {err:?}");
8327 };
8328 assert_eq!(slot, ":bibliotecas");
8329 assert_eq!(path, PathBuf::from("lib/x.lisp"));
8330 }
8331
8332 #[test]
8333 fn validate_code_paths_duplicate_diagnostic_carries_offending_slot_and_path() {
8334 // Diagnostic-shape pin (peer with
8335 // `validate_code_paths_diagnostic_carries_offending_slot_and_path`
8336 // on the structural arm): the duplicate-arm Display surfaces both
8337 // the offending `:slot` tag and the offending path verbatim, so a
8338 // `feira lint` run can render the diagnostic without re-parsing.
8339 let c = caixa_with_code_paths(
8340 vec![],
8341 vec![],
8342 vec![
8343 "servicos/demo.computeunit.yaml",
8344 "servicos/demo.computeunit.yaml",
8345 ],
8346 );
8347 let rendered = c.validate_code_paths().unwrap_err().to_string();
8348 assert!(
8349 rendered.contains(":servicos"),
8350 "diagnostic must name the offending slot: {rendered}",
8351 );
8352 assert!(
8353 rendered.contains("servicos/demo.computeunit.yaml"),
8354 "diagnostic must quote the offending path: {rendered}",
8355 );
8356 }
8357
8358 // ── validate_code_paths — `.lisp` extension gate on :bibliotecas ──
8359 //
8360 // The lifted [`crate::render::is_lisp_extension`] predicate (33cc830)
8361 // now gates `:bibliotecas` entries on the tatara-lisp-source file-type
8362 // contract. The `feira build` loop (`caixa-feira/src/cmd/build.rs:33`)
8363 // reads every declared `:bibliotecas` entry through `tatara_lisp::read`
8364 // at parse time — the same downstream consumer the peer `:behavior
8365 // :on-*` (c97815a, [`crate::BehaviorError::NonLispExtension`]) and
8366 // `:upgrade-from :state-change :script` (33cc830,
8367 // [`crate::UpgradeError::NonLispExtensionScript`]) axes route through.
8368 // `:exe` and `:servicos` are deliberately excluded — `:exe` is the
8369 // nix-built executable surface (`"exe/<name>"` shape per the canonical
8370 // [`crate::LayoutError::ExeOutsideDir`] error message and every
8371 // in-tree `caixa_with_code_paths` positive control), and `:servicos`
8372 // is the `.computeunit.yaml` ComputeUnit-CR axis.
8373
8374 #[test]
8375 fn validate_code_paths_rejects_no_extension_bibliotecas_entry() {
8376 // Canonical "I dragged the wrong file from the workspace tree"
8377 // footgun on the biblioteca axis. Without the gate `feira build`
8378 // hands the extensionless path to `tatara_lisp::read` and fails
8379 // with a parser-shaped diagnostic far from the source caixa.lisp,
8380 // with no field naming the offending `:bibliotecas` entry.
8381 for relpath in ["lib/demo", "demo", "lib/handlers/inner"] {
8382 let c = caixa_with_code_paths(vec![relpath], vec![], vec![]);
8383 let err = c.validate_code_paths().unwrap_err();
8384 let ManifestError::CodePathNonLispExtension { slot, path } = err else {
8385 panic!("expected CodePathNonLispExtension for {relpath:?}, got {err:?}");
8386 };
8387 assert_eq!(slot, ":bibliotecas");
8388 assert_eq!(path, PathBuf::from(relpath));
8389 }
8390 }
8391
8392 #[test]
8393 fn validate_code_paths_rejects_wrong_extension_bibliotecas_entry() {
8394 // Wrong-extension sweep across common authoring footguns. Same
8395 // sweep posture as the peer
8396 // `behavior::validate_rejects_wrong_extension` (c97815a) and
8397 // `upgrade::tests::state_change_rejects_wrong_extension_script`
8398 // (33cc830) cases.
8399 for relpath in [
8400 "lib/demo.rs",
8401 "lib/demo.txt",
8402 "lib/demo.md",
8403 "lib/demo.json",
8404 "lib/demo.yaml",
8405 "lib/demo.toml",
8406 "lib/demo.lisp.bak",
8407 "lib/demo.lispx",
8408 "lib/demo.lis",
8409 ] {
8410 let c = caixa_with_code_paths(vec![relpath], vec![], vec![]);
8411 let err = c.validate_code_paths().unwrap_err();
8412 let ManifestError::CodePathNonLispExtension { slot, path } = err else {
8413 panic!("expected CodePathNonLispExtension for {relpath:?}, got {err:?}");
8414 };
8415 assert_eq!(slot, ":bibliotecas");
8416 assert_eq!(path, PathBuf::from(relpath));
8417 }
8418 }
8419
8420 #[test]
8421 fn validate_code_paths_rejects_case_folded_extension_bibliotecas_entry() {
8422 // Case-sensitivity sweep — pins the strict lowercase `.lisp`
8423 // contract. An uppercase `.LISP` shape that the layout's existence
8424 // check would (case-insensitively, on case-insensitive volumes)
8425 // match the on-disk file still mismatches the canonical form the
8426 // codec emits, breaking the THEORY.md §V.2.7 render-determinism
8427 // contract. Mirrors the peer
8428 // `behavior::validate_rejects_case_folded_extension` (c97815a) and
8429 // `upgrade::tests::state_change_rejects_case_folded_extension_script`
8430 // (33cc830) sweeps.
8431 for relpath in [
8432 "lib/demo.LISP",
8433 "lib/demo.Lisp",
8434 "lib/demo.LiSp",
8435 "lib/demo.lISP",
8436 ] {
8437 let c = caixa_with_code_paths(vec![relpath], vec![], vec![]);
8438 let err = c.validate_code_paths().unwrap_err();
8439 let ManifestError::CodePathNonLispExtension { slot, path } = err else {
8440 panic!("expected CodePathNonLispExtension for {relpath:?}, got {err:?}");
8441 };
8442 assert_eq!(slot, ":bibliotecas");
8443 assert_eq!(path, PathBuf::from(relpath));
8444 }
8445 }
8446
8447 #[test]
8448 fn validate_code_paths_accepts_canonical_lisp_shapes() {
8449 // Positive-control sweep through every canonical authoring shape
8450 // every in-tree fixture and the `Caixa::template` scaffold use.
8451 // Mirrors the peer `behavior::validate_accepts_canonical_lisp_paths`
8452 // (c97815a) and the lifted predicate's own
8453 // `is_lisp_extension_accepts_canonical_shapes` sweep in render.rs
8454 // (33cc830).
8455 for relpath in [
8456 "lib/demo.lisp",
8457 "lib/handlers.lisp",
8458 "lib/migrations/v01-to-v02.lisp",
8459 "demo.lisp",
8460 "a.lisp",
8461 "./lib/demo.lisp",
8462 "lib/./handlers.lisp",
8463 "lib/migrations/v.0.1.lisp",
8464 ] {
8465 let c = caixa_with_code_paths(vec![relpath], vec![], vec![]);
8466 c.validate_code_paths()
8467 .unwrap_or_else(|e| panic!("canonical shape {relpath:?} must pass, got {e:?}"));
8468 }
8469 }
8470
8471 #[test]
8472 fn validate_code_paths_non_lisp_extension_does_not_fire_on_exe_or_servicos() {
8473 // The file-type gate is per-slot — only `:bibliotecas` carries the
8474 // tatara-lisp-source contract. An extensionless `:exe` entry
8475 // (`exe/demo`) and a `.computeunit.yaml` `:servicos` entry are the
8476 // canonical shapes every in-tree fixture uses, and must continue
8477 // to pass validate. Pins that a future tightening that broadens
8478 // the `.lisp` gate to either axis surfaces as a test failure
8479 // rather than as a silent breaking change to existing valid
8480 // manifests.
8481 let c = caixa_with_code_paths(
8482 vec![],
8483 vec!["exe/demo", "exe/tool"],
8484 vec!["servicos/demo.computeunit.yaml"],
8485 );
8486 c.validate_code_paths().unwrap();
8487 }
8488
8489 #[test]
8490 fn validate_code_paths_sandbox_shape_arms_precede_non_lisp_extension() {
8491 // Cross-arm precedence pin: a `:bibliotecas` entry that is *both*
8492 // sandbox-escaping and non-`.lisp` surfaces the more fundamental
8493 // sandbox-shape diagnostic first (the `.lisp` remediation would
8494 // be misleading when the offending path can never resolve under
8495 // the caixa root anyway). Mirrors the peer
8496 // `EmptyPath` → `AbsolutePath` → `ParentEscape` → `NonLispExtension`
8497 // ordering on `:behavior :on-*` (c97815a) and `EmptyScript` →
8498 // `AbsoluteScript` → `ParentEscapeScript` → `NonLispExtensionScript`
8499 // on `:upgrade-from :state-change :script` (33cc830).
8500 //
8501 // Empty wins (the strictly-smaller-scope structural arm).
8502 let c = caixa_with_code_paths(vec![""], vec![], vec![]);
8503 assert!(
8504 matches!(
8505 c.validate_code_paths().unwrap_err(),
8506 ManifestError::CodePathEmpty {
8507 slot: ":bibliotecas"
8508 }
8509 ),
8510 "empty must win over non-lisp-extension",
8511 );
8512 // Absolute wins (the path can't resolve under the caixa root).
8513 let c = caixa_with_code_paths(vec!["/etc/passwd"], vec![], vec![]);
8514 let err = c.validate_code_paths().unwrap_err();
8515 let ManifestError::CodePathAbsolute { slot, .. } = err else {
8516 panic!("absolute must win over non-lisp-extension, got {err:?}");
8517 };
8518 assert_eq!(slot, ":bibliotecas");
8519 // ParentEscape wins (the path escapes the caixa root).
8520 let c = caixa_with_code_paths(vec!["../sibling/x.txt"], vec![], vec![]);
8521 let err = c.validate_code_paths().unwrap_err();
8522 let ManifestError::CodePathParentEscape { slot, .. } = err else {
8523 panic!("parent-escape must win over non-lisp-extension, got {err:?}");
8524 };
8525 assert_eq!(slot, ":bibliotecas");
8526 }
8527
8528 #[test]
8529 fn validate_code_paths_non_lisp_extension_precedes_duplicate() {
8530 // Within-slot precedence pin: the per-entry file-type shape gate
8531 // fires before the cross-entry duplicate gate, so the narrower
8532 // structural defect dominates the uniqueness diagnostic. A
8533 // `("lib/x.txt" "lib/x.txt")` shape surfaces
8534 // `CodePathNonLispExtension` on the first entry rather than
8535 // `CodePathDuplicate` on the pair — same posture every per-entry
8536 // shape-gate-precedes-duplicate cascade follows on this surface
8537 // (the empty / absolute / parent-escape arms already precede the
8538 // duplicate arm; the lifted file-type arm joins that set).
8539 let c = caixa_with_code_paths(vec!["lib/x.txt", "lib/x.txt"], vec![], vec![]);
8540 let err = c.validate_code_paths().unwrap_err();
8541 let ManifestError::CodePathNonLispExtension { slot, path } = err else {
8542 panic!("expected CodePathNonLispExtension, got {err:?}");
8543 };
8544 assert_eq!(slot, ":bibliotecas");
8545 assert_eq!(path, PathBuf::from("lib/x.txt"));
8546 }
8547
8548 #[test]
8549 fn validate_code_paths_non_lisp_extension_diagnostic_carries_offending_slot_and_path() {
8550 // Diagnostic-shape pin (peer with
8551 // `validate_code_paths_diagnostic_carries_offending_slot_and_path`
8552 // on the sandbox-shape arms and
8553 // `validate_code_paths_duplicate_diagnostic_carries_offending_slot_and_path`
8554 // on the duplicate arm): the file-type-arm Display surfaces both
8555 // the offending `:slot` tag, the offending path verbatim, and the
8556 // expected `.lisp` extension named in the remediation text, so a
8557 // `feira lint` run can render the diagnostic without re-parsing.
8558 let c = caixa_with_code_paths(vec!["lib/demo.rs"], vec![], vec![]);
8559 let rendered = c.validate_code_paths().unwrap_err().to_string();
8560 assert!(
8561 rendered.contains(":bibliotecas"),
8562 "diagnostic must name the offending slot: {rendered}",
8563 );
8564 assert!(
8565 rendered.contains("lib/demo.rs"),
8566 "diagnostic must quote the offending path: {rendered}",
8567 );
8568 assert!(
8569 rendered.contains(".lisp"),
8570 "diagnostic must name the expected extension: {rendered}",
8571 );
8572 }
8573
8574 // ── validate_code_paths — `.computeunit.yaml` compound-suffix gate on :servicos ──
8575 //
8576 // The lifted [`crate::render::is_computeunit_yaml_extension`] predicate
8577 // now gates `:servicos` entries on the ComputeUnit-CR YAML file-type
8578 // contract. The peer caixa-helm / caixa-flux renderers consume each
8579 // `:servicos` entry through `serde_yaml::from_str` as a typed
8580 // `ComputeUnit` CR — same downstream-consumer-shape lift as the peer
8581 // `:bibliotecas` `.lisp` gate (64772a9), here on the compound-suffix
8582 // axis `Path::extension` can't express on its own.
8583
8584 #[test]
8585 fn validate_code_paths_rejects_no_extension_servicos_entry() {
8586 // Canonical "I dragged the wrong file from the workspace tree"
8587 // footgun on the Servico axis. Without the gate the peer
8588 // caixa-helm / caixa-flux renderers hand the extensionless path
8589 // to `serde_yaml::from_str` and fail with a parser-shaped
8590 // diagnostic far from the source caixa.lisp, with no field
8591 // naming the offending `:servicos` entry.
8592 for relpath in ["servicos/demo", "demo", "servicos/sub/nested"] {
8593 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
8594 let err = c.validate_code_paths().unwrap_err();
8595 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
8596 panic!(
8597 "expected CodePathNonComputeUnitYamlExtension for {relpath:?}, \
8598 got {err:?}"
8599 );
8600 };
8601 assert_eq!(slot, ":servicos");
8602 assert_eq!(path, PathBuf::from(relpath));
8603 }
8604 }
8605
8606 #[test]
8607 fn validate_code_paths_rejects_wrong_extension_servicos_entry() {
8608 // Wrong-extension sweep across common authoring footguns on the
8609 // Servico axis. Bare `.yaml` is the canonical "I forgot the
8610 // `.computeunit` segment" typo; the off-by-one-segment shapes
8611 // (`.computeunit-yaml` / `.computeunit_yaml`) silently pass the
8612 // bare `Path::extension` view but mismatch the typed compound
8613 // suffix the renderers' `serde_yaml::from_str` consumer demands.
8614 // Same sweep-posture as the peer
8615 // `validate_code_paths_rejects_wrong_extension_bibliotecas_entry`
8616 // (64772a9) on the sibling tatara-lisp-source axis.
8617 for relpath in [
8618 "servicos/demo.yaml",
8619 "servicos/demo.yml",
8620 "servicos/demo.json",
8621 "servicos/demo.toml",
8622 "servicos/demo.txt",
8623 "servicos/demo.computeunit.yaml.bak",
8624 "servicos/demo.computeunit.yam",
8625 "servicos/demo.computeunit",
8626 "servicos/demo-computeunit.yaml",
8627 "servicos/demo_computeunit.yaml",
8628 ] {
8629 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
8630 let err = c.validate_code_paths().unwrap_err();
8631 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
8632 panic!(
8633 "expected CodePathNonComputeUnitYamlExtension for {relpath:?}, \
8634 got {err:?}"
8635 );
8636 };
8637 assert_eq!(slot, ":servicos");
8638 assert_eq!(path, PathBuf::from(relpath));
8639 }
8640 }
8641
8642 #[test]
8643 fn validate_code_paths_rejects_case_folded_extension_servicos_entry() {
8644 // Case-sensitivity sweep — pins the strict lowercase
8645 // `.computeunit.yaml` contract. A case-folded shape that the
8646 // layout's existence check would (case-insensitively, on
8647 // case-insensitive volumes) match the on-disk file still
8648 // mismatches the canonical form the codec emits, breaking the
8649 // THEORY.md §V.2.7 render-determinism contract. Mirrors the peer
8650 // `validate_code_paths_rejects_case_folded_extension_bibliotecas_entry`
8651 // (64772a9) sweep on the sibling tatara-lisp-source axis.
8652 for relpath in [
8653 "servicos/demo.ComputeUnit.yaml",
8654 "servicos/demo.COMPUTEUNIT.yaml",
8655 "servicos/demo.computeunit.YAML",
8656 "servicos/demo.computeunit.Yaml",
8657 "servicos/demo.COMPUTEUNIT.YAML",
8658 ] {
8659 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
8660 let err = c.validate_code_paths().unwrap_err();
8661 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
8662 panic!(
8663 "expected CodePathNonComputeUnitYamlExtension for {relpath:?}, \
8664 got {err:?}"
8665 );
8666 };
8667 assert_eq!(slot, ":servicos");
8668 assert_eq!(path, PathBuf::from(relpath));
8669 }
8670 }
8671
8672 #[test]
8673 fn validate_code_paths_rejects_empty_stem_servicos_entry() {
8674 // Degenerate hidden-file shape: a file name exactly equal to the
8675 // suffix (`.computeunit.yaml` — no stem preceding the suffix) is
8676 // the structural "Servico declared with no identity" footgun.
8677 // The substrate identifies each ComputeUnit by the file-stem
8678 // segment that precedes `.computeunit.yaml` (the rendered
8679 // `lareira-<stem>` Helm chart, the per-Servico `metadata.name`,
8680 // the M3 `:contratos` membership lookup), so an empty stem
8681 // leaves the Servico unidentifiable. Pinned at the typed-axis
8682 // level so a future regression that drops the `name.len() >
8683 // SUFFIX.len()` bound at the predicate surfaces here, not
8684 // piecemeal as a `lareira-` chart-name collision at render time.
8685 for relpath in ["servicos/.computeunit.yaml"] {
8686 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
8687 let err = c.validate_code_paths().unwrap_err();
8688 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
8689 panic!(
8690 "expected CodePathNonComputeUnitYamlExtension for {relpath:?}, \
8691 got {err:?}"
8692 );
8693 };
8694 assert_eq!(slot, ":servicos");
8695 assert_eq!(path, PathBuf::from(relpath));
8696 }
8697 }
8698
8699 #[test]
8700 fn validate_code_paths_accepts_canonical_computeunit_yaml_shapes() {
8701 // Positive-control sweep through every canonical authoring shape
8702 // every in-tree fixture and the `Caixa::template` scaffold use.
8703 // Mirrors the peer
8704 // `validate_code_paths_accepts_canonical_lisp_shapes` (64772a9)
8705 // and the lifted predicate's own
8706 // `computeunit_yaml_extension_accepts_canonical_shapes` sweep in
8707 // render.rs.
8708 for relpath in [
8709 "servicos/demo.computeunit.yaml",
8710 "servicos/hello-rio.computeunit.yaml",
8711 "servicos/my-service.computeunit.yaml",
8712 "servicos/a.computeunit.yaml",
8713 "./servicos/demo.computeunit.yaml",
8714 "servicos/./demo.computeunit.yaml",
8715 "servicos/sub/nested.computeunit.yaml",
8716 "servicos/v0.1.computeunit.yaml",
8717 ] {
8718 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
8719 c.validate_code_paths()
8720 .unwrap_or_else(|e| panic!("canonical shape {relpath:?} must pass, got {e:?}"));
8721 }
8722 }
8723
8724 #[test]
8725 fn validate_code_paths_non_computeunit_yaml_extension_does_not_fire_on_bibliotecas_or_exe() {
8726 // The file-type gate is per-slot — only `:servicos` carries the
8727 // ComputeUnit-CR YAML contract. A canonical `.lisp` `:bibliotecas`
8728 // entry and an extensionless `:exe` entry are the canonical
8729 // shapes every in-tree fixture uses, and must continue to pass
8730 // validate. Peer of
8731 // `validate_code_paths_non_lisp_extension_does_not_fire_on_exe_or_servicos`
8732 // (64772a9) — together pin that the typed
8733 // [`CodePathFileType`] dispatch is exhaustively per-slot, with no
8734 // cross-axis leakage in either direction.
8735 let c = caixa_with_code_paths(
8736 vec!["lib/demo.lisp"],
8737 vec!["exe/demo", "exe/tool"],
8738 vec!["servicos/demo.computeunit.yaml"],
8739 );
8740 c.validate_code_paths().unwrap();
8741 }
8742
8743 #[test]
8744 fn validate_code_paths_sandbox_shape_arms_precede_non_computeunit_yaml_extension() {
8745 // Cross-arm precedence pin: a `:servicos` entry that is *both*
8746 // sandbox-escaping and wrong-extension surfaces the more
8747 // fundamental sandbox-shape diagnostic first (the
8748 // `.computeunit.yaml` remediation would be misleading when the
8749 // offending path can never resolve under the caixa root
8750 // anyway). Mirrors the peer
8751 // `validate_code_paths_sandbox_shape_arms_precede_non_lisp_extension`
8752 // (64772a9) ordering on the sibling `:bibliotecas` axis and the
8753 // peer `EmptyPath` → `AbsolutePath` → `ParentEscape` →
8754 // `NonComputeUnitYamlExtension` arm-ordering the dispatch
8755 // table establishes.
8756 //
8757 // Empty wins (the strictly-smaller-scope structural arm).
8758 let c = caixa_with_code_paths(vec![], vec![], vec![""]);
8759 assert!(
8760 matches!(
8761 c.validate_code_paths().unwrap_err(),
8762 ManifestError::CodePathEmpty { slot: ":servicos" }
8763 ),
8764 "empty must win over non-computeunit-yaml-extension",
8765 );
8766 // Absolute wins (the path can't resolve under the caixa root).
8767 let c = caixa_with_code_paths(vec![], vec![], vec!["/etc/foo.yaml"]);
8768 let err = c.validate_code_paths().unwrap_err();
8769 let ManifestError::CodePathAbsolute { slot, .. } = err else {
8770 panic!("absolute must win over non-computeunit-yaml-extension, got {err:?}");
8771 };
8772 assert_eq!(slot, ":servicos");
8773 // ParentEscape wins (the path escapes the caixa root).
8774 let c = caixa_with_code_paths(vec![], vec![], vec!["../sibling/x.yaml"]);
8775 let err = c.validate_code_paths().unwrap_err();
8776 let ManifestError::CodePathParentEscape { slot, .. } = err else {
8777 panic!("parent-escape must win over non-computeunit-yaml-extension, got {err:?}");
8778 };
8779 assert_eq!(slot, ":servicos");
8780 }
8781
8782 #[test]
8783 fn validate_code_paths_non_computeunit_yaml_extension_precedes_duplicate() {
8784 // Within-slot precedence pin: the per-entry file-type shape gate
8785 // fires before the cross-entry duplicate gate, so the narrower
8786 // structural defect dominates the uniqueness diagnostic. A
8787 // `("servicos/x.yaml" "servicos/x.yaml")` shape surfaces
8788 // `CodePathNonComputeUnitYamlExtension` on the first entry
8789 // rather than `CodePathDuplicate` on the pair — same posture
8790 // every per-entry shape-gate-precedes-duplicate cascade follows
8791 // on this surface, peer of the 64772a9 `:bibliotecas`
8792 // `("lib/x.txt" "lib/x.txt")` ordering.
8793 let c = caixa_with_code_paths(vec![], vec![], vec!["servicos/x.yaml", "servicos/x.yaml"]);
8794 let err = c.validate_code_paths().unwrap_err();
8795 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
8796 panic!("expected CodePathNonComputeUnitYamlExtension, got {err:?}");
8797 };
8798 assert_eq!(slot, ":servicos");
8799 assert_eq!(path, PathBuf::from("servicos/x.yaml"));
8800 }
8801
8802 #[test]
8803 fn validate_code_paths_non_computeunit_yaml_extension_diagnostic_carries_offending_slot_and_path()
8804 {
8805 // Diagnostic-shape pin (peer with
8806 // `validate_code_paths_non_lisp_extension_diagnostic_carries_offending_slot_and_path`
8807 // on the sibling tatara-lisp-source axis): the file-type-arm
8808 // Display surfaces both the offending `:slot` tag, the
8809 // offending path verbatim, and the expected
8810 // `.computeunit.yaml` compound suffix named in the remediation
8811 // text, so a `feira lint` run can render the diagnostic without
8812 // re-parsing.
8813 let c = caixa_with_code_paths(vec![], vec![], vec!["servicos/demo.yaml"]);
8814 let rendered = c.validate_code_paths().unwrap_err().to_string();
8815 assert!(
8816 rendered.contains(":servicos"),
8817 "diagnostic must name the offending slot: {rendered}",
8818 );
8819 assert!(
8820 rendered.contains("servicos/demo.yaml"),
8821 "diagnostic must quote the offending path: {rendered}",
8822 );
8823 assert!(
8824 rendered.contains(".computeunit.yaml"),
8825 "diagnostic must name the expected compound suffix: {rendered}",
8826 );
8827 }
8828
8829 // ── validate_etiquetas — universal-axis registry-search-tag shape ──
8830
8831 fn caixa_with_etiquetas(etiquetas: Vec<&str>) -> Caixa {
8832 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
8833 c.etiquetas = etiquetas.into_iter().map(String::from).collect();
8834 c
8835 }
8836
8837 #[test]
8838 fn validate_etiquetas_accepts_empty_list() {
8839 // The empty-list identity: every caixa with no declared tags
8840 // trivially passes — `Caixa::template` emits `:etiquetas ()`,
8841 // so the gate is non-disruptive against every existing manifest.
8842 let c = caixa_with_etiquetas(vec![]);
8843 c.validate_etiquetas().unwrap();
8844 }
8845
8846 #[test]
8847 fn validate_etiquetas_accepts_canonical_forms() {
8848 // Positive control sweep: a canonical-shaped non-empty distinct
8849 // tag list passes, mirroring the example checkout-aplicacao
8850 // (`:etiquetas ("example" "aplicacao" "mesh" "ecommerce" "demo")`)
8851 // and the hello-rio fixture (`("hello-world" "wasm" "rust")`).
8852 let c = caixa_with_etiquetas(vec!["example", "aplicacao", "mesh", "ecommerce", "demo"]);
8853 c.validate_etiquetas().unwrap();
8854 }
8855
8856 #[test]
8857 fn validate_etiquetas_rejects_empty_entry() {
8858 // Canonical paste-from-blank-doc footgun. Without the gate the
8859 // empty entry rendered as `keywords: [""]` in `Chart.yaml`, a
8860 // no-op tag indexing nothing in the future caixa-registry.
8861 let c = caixa_with_etiquetas(vec![""]);
8862 let err = c.validate_etiquetas().unwrap_err();
8863 assert!(matches!(err, ManifestError::EtiquetaEmpty), "got {err:?}",);
8864 }
8865
8866 #[test]
8867 fn validate_etiquetas_rejects_duplicate_entry() {
8868 // Canonical copy-paste-the-wrong-tag footgun. Without the gate
8869 // the duplicate was silently dedup'd by caixa-helm's BTreeSet
8870 // collect at chart render — a "second wins / one silently
8871 // disappears" shape divergent from every peer typed-graph set
8872 // gate. The duplicate-arm names the offending tag verbatim.
8873 let c = caixa_with_etiquetas(vec!["demo", "demo"]);
8874 let err = c.validate_etiquetas().unwrap_err();
8875 let ManifestError::EtiquetaDuplicate { etiqueta } = err else {
8876 panic!("expected EtiquetaDuplicate, got {err:?}");
8877 };
8878 assert_eq!(etiqueta, "demo");
8879 }
8880
8881 #[test]
8882 fn validate_etiquetas_empty_takes_precedence_over_duplicate() {
8883 // Empty-first cascade pin: `("" "demo" "demo")` surfaces
8884 // `EtiquetaEmpty` not `EtiquetaDuplicate` — the narrower
8885 // structural "this entry has no value" defect dominates the
8886 // cross-entry uniqueness diagnostic. Mirrors the peer
8887 // empty-before-duplicate cascades on `:caracteristicas`
8888 // (`CaracteristicaEmpty` before `CaracteristicaDuplicate`,
8889 // fc3b4d5) and `:membros :caixa` (`MembroCaixaEmpty` before
8890 // `MembroDuplicate`).
8891 let c = caixa_with_etiquetas(vec!["", "demo", "demo"]);
8892 let err = c.validate_etiquetas().unwrap_err();
8893 assert!(matches!(err, ManifestError::EtiquetaEmpty), "got {err:?}",);
8894 }
8895
8896 #[test]
8897 fn validate_etiquetas_duplicate_reports_first_collision() {
8898 // First-collision pin: `("a" "b" "a" "b")` surfaces the `"a"`
8899 // duplicate (the lexicographically-earliest offending position
8900 // — the second `"a"` at index 2 collides with the first `"a"`
8901 // at index 0), not the later `"b"` collision at index 3,
8902 // peer with every other first-collision diagnostic posture on
8903 // this surface (`validate_load_singularity_reports_first_collision`,
8904 // `validate_cleanup_singularity_reports_first_collision`).
8905 let c = caixa_with_etiquetas(vec!["a", "b", "a", "b"]);
8906 let err = c.validate_etiquetas().unwrap_err();
8907 let ManifestError::EtiquetaDuplicate { etiqueta } = err else {
8908 panic!("expected EtiquetaDuplicate, got {err:?}");
8909 };
8910 assert_eq!(etiqueta, "a");
8911 }
8912
8913 #[test]
8914 fn validate_etiquetas_case_sensitive() {
8915 // Case-sensitivity pin: `("Foo" "foo")` is two distinct entries,
8916 // mirroring the peer `:membros :caixa` / `:children :caixa`
8917 // exact-string-match discipline. The shape gate this routine
8918 // landed (`is_chart_keyword_shape`, Cargo crates.io keyword
8919 // grammar) accepts mixed case — crates.io's keyword rule is
8920 // "case-insensitive" at the index layer but admits mixed case
8921 // at the entry layer (the canonical Helm chart `keywords:`
8922 // shape is lowercase by convention, but the grammar admits
8923 // uppercase). Case-sensitivity at the duplicate-set layer
8924 // remains structural — two distinct strings are two distinct
8925 // entries.
8926 let c = caixa_with_etiquetas(vec!["Foo", "foo"]);
8927 c.validate_etiquetas().unwrap();
8928 }
8929
8930 #[test]
8931 fn validate_etiquetas_diagnostic_carries_offending_tag() {
8932 // Diagnostic-shape pin (peer with
8933 // `validate_code_paths_diagnostic_carries_offending_slot_and_path`):
8934 // the error's Display surfaces the offending tag verbatim, so a
8935 // `feira lint` run can render the diagnostic without re-parsing
8936 // and the author can grep their caixa.lisp for the offending
8937 // value.
8938 let c = caixa_with_etiquetas(vec!["demo", "demo"]);
8939 let rendered = c.validate_etiquetas().unwrap_err().to_string();
8940 assert!(
8941 rendered.contains(":etiquetas"),
8942 "diagnostic must name the offending slot: {rendered}",
8943 );
8944 assert!(
8945 rendered.contains("demo"),
8946 "diagnostic must quote the offending tag: {rendered}",
8947 );
8948 }
8949
8950 #[test]
8951 fn validate_etiquetas_rejects_leading_whitespace_entry() {
8952 // Canonical paste-from-aligned-doc footgun. Without the shape
8953 // gate `" mesh"` silently passed validate and landed as a
8954 // YAML plain-style scalar with leading whitespace in the
8955 // rendered Chart.yaml `keywords:` array — every YAML 1.2
8956 // dumper trims leading whitespace from plain-style scalars,
8957 // so the authored space round-tripped inconsistently back
8958 // through `caixa.lisp`. Mirrors the peer
8959 // `validate_autores_rejects_leading_whitespace_entry`.
8960 let c = caixa_with_etiquetas(vec![" mesh"]);
8961 let err = c.validate_etiquetas().unwrap_err();
8962 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8963 panic!("expected EtiquetaInvalid, got {err:?}");
8964 };
8965 assert_eq!(etiqueta, " mesh");
8966 assert!(reason.contains("whitespace"), "got: {reason}");
8967 }
8968
8969 #[test]
8970 fn validate_etiquetas_rejects_embedded_newline_entry() {
8971 // Canonical paste-from-multiline-doc footgun — the author
8972 // pasted a multi-tag block into one `:etiquetas` entry
8973 // instead of splitting into one entry per tag. Without the
8974 // shape gate `"mesh\nhttp"` silently passed validate and
8975 // landed as a YAML-illegal multi-line scalar in the rendered
8976 // Chart.yaml `keywords:` array.
8977 let c = caixa_with_etiquetas(vec!["mesh\nhttp"]);
8978 let err = c.validate_etiquetas().unwrap_err();
8979 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8980 panic!("expected EtiquetaInvalid, got {err:?}");
8981 };
8982 assert_eq!(etiqueta, "mesh\nhttp");
8983 assert!(reason.contains("newline"), "got: {reason}");
8984 }
8985
8986 #[test]
8987 fn validate_etiquetas_rejects_embedded_comma_entry() {
8988 // Canonical CSV-list-separator-confusion footgun: the author
8989 // confused the CSV-style separator convention with the
8990 // `:etiquetas` list grammar. Without the shape gate
8991 // `"mesh,http,grpc"` silently passed validate and landed as a
8992 // single malformed search tag in the rendered Chart.yaml
8993 // `keywords:` array — Artifact Hub's keyword index would
8994 // either silently drop the tag or index it as
8995 // `mesh,http,grpc` instead of three separate tags.
8996 let c = caixa_with_etiquetas(vec!["mesh,http,grpc"]);
8997 let err = c.validate_etiquetas().unwrap_err();
8998 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8999 panic!("expected EtiquetaInvalid, got {err:?}");
9000 };
9001 assert_eq!(etiqueta, "mesh,http,grpc");
9002 assert!(reason.contains('`'), "got: {reason}");
9003 assert!(reason.contains(','), "got: {reason}");
9004 }
9005
9006 #[test]
9007 fn validate_etiquetas_rejects_embedded_slash_entry() {
9008 // Canonical path-separator-confusion footgun: the author
9009 // confused namespace-path notation with the keyword grammar.
9010 let c = caixa_with_etiquetas(vec!["caixa/servico"]);
9011 let err = c.validate_etiquetas().unwrap_err();
9012 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
9013 panic!("expected EtiquetaInvalid, got {err:?}");
9014 };
9015 assert_eq!(etiqueta, "caixa/servico");
9016 assert!(reason.contains('/'), "got: {reason}");
9017 }
9018
9019 #[test]
9020 fn validate_etiquetas_rejects_leading_digit_entry() {
9021 // Canonical paste-from-numbered-list footgun: the author
9022 // copied `1. mesh` from a numbered doc and the `1` leaked
9023 // into the tag.
9024 let c = caixa_with_etiquetas(vec!["1mesh"]);
9025 let err = c.validate_etiquetas().unwrap_err();
9026 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
9027 panic!("expected EtiquetaInvalid, got {err:?}");
9028 };
9029 assert_eq!(etiqueta, "1mesh");
9030 assert!(reason.contains("digit"), "got: {reason}");
9031 }
9032
9033 #[test]
9034 fn validate_etiquetas_rejects_leading_hyphen_entry() {
9035 // Canonical kebab-leak footgun.
9036 let c = caixa_with_etiquetas(vec!["-foo"]);
9037 let err = c.validate_etiquetas().unwrap_err();
9038 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
9039 panic!("expected EtiquetaInvalid, got {err:?}");
9040 };
9041 assert_eq!(etiqueta, "-foo");
9042 assert!(reason.contains('-'), "got: {reason}");
9043 }
9044
9045 #[test]
9046 fn validate_etiquetas_rejects_non_ascii_entry() {
9047 // Canonical paste-from-Unicode-doc footgun. Every legitimate
9048 // search tag is strict ASCII; raw non-ASCII silently
9049 // round-trips inconsistently across NFC/NFD normalization on
9050 // APFS / case-folding filesystems and breaks the Artifact Hub
9051 // keyword search index lookup.
9052 let c = caixa_with_etiquetas(vec!["café"]);
9053 let err = c.validate_etiquetas().unwrap_err();
9054 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
9055 panic!("expected EtiquetaInvalid, got {err:?}");
9056 };
9057 assert_eq!(etiqueta, "café");
9058 assert!(reason.contains("non-ASCII"), "got: {reason}");
9059 }
9060
9061 #[test]
9062 fn validate_etiquetas_rejects_period_entry() {
9063 // Canonical namespace-confusion / version-suffix footgun
9064 // (`"http.1"` / `"v1.0"`): Cargo's crates.io keyword grammar
9065 // excludes `.` from the continuation set even though the
9066 // sibling `:caracteristicas` axis (Cargo's feature-name
9067 // grammar) admits it. Tighter than the sibling axis, peer
9068 // with Cargo's own crates.io keyword shape.
9069 let c = caixa_with_etiquetas(vec!["http.1"]);
9070 let err = c.validate_etiquetas().unwrap_err();
9071 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
9072 panic!("expected EtiquetaInvalid, got {err:?}");
9073 };
9074 assert_eq!(etiqueta, "http.1");
9075 assert!(reason.contains('.'), "got: {reason}");
9076 }
9077
9078 #[test]
9079 fn validate_etiquetas_empty_takes_precedence_over_shape() {
9080 // Per-entry empty-first cascade pin: an entry that is both
9081 // empty *and* shape-invalid surfaces `EtiquetaEmpty` (the
9082 // narrower "this entry has no value" structural defect
9083 // dominates the broader shape-predicate diagnostic). The
9084 // empty arm fires before the shape predicate is consulted,
9085 // mirroring the peer `validate_autores_empty_takes_precedence_over_shape`
9086 // cascade established on the sibling universal-axis Vec<String>
9087 // surface.
9088 let c = caixa_with_etiquetas(vec![""]);
9089 let err = c.validate_etiquetas().unwrap_err();
9090 assert!(matches!(err, ManifestError::EtiquetaEmpty), "got {err:?}",);
9091 }
9092
9093 #[test]
9094 fn validate_etiquetas_shape_takes_precedence_over_duplicate() {
9095 // Per-entry shape-before-cross-entry-duplicate cascade pin: an
9096 // entry that is malformed surfaces `EtiquetaInvalid` even when
9097 // a later entry would have collided on duplicate. The
9098 // per-entry shape arm fires inside the same loop iteration as
9099 // the empty arm, before the seen-set insert at end-of-iteration
9100 // — structural per-entry defects dominate the cross-entry
9101 // uniqueness diagnostic. Mirrors the peer
9102 // `validate_autores_shape_takes_precedence_over_duplicate`.
9103 let c = caixa_with_etiquetas(vec!["mesh\nhttp", "mesh\nhttp"]);
9104 let err = c.validate_etiquetas().unwrap_err();
9105 assert!(
9106 matches!(err, ManifestError::EtiquetaInvalid { .. }),
9107 "got {err:?}",
9108 );
9109 }
9110
9111 #[test]
9112 fn validate_etiquetas_invalid_diagnostic_names_offending_slot_and_value() {
9113 // Diagnostic-shape pin on the new shape arm (peer with
9114 // `validate_autores_invalid_diagnostic_names_offending_slot_and_value`):
9115 // the rendered Display surfaces both the offending slot name
9116 // and the offending value verbatim, so a `feira lint` run
9117 // points the author at the exact `:etiquetas` entry to fix.
9118 let c = caixa_with_etiquetas(vec!["mesh\nhttp"]);
9119 let rendered = c.validate_etiquetas().unwrap_err().to_string();
9120 assert!(
9121 rendered.contains(":etiquetas"),
9122 "diagnostic must name the offending slot: {rendered}",
9123 );
9124 assert!(
9125 rendered.contains("mesh\\nhttp"),
9126 "diagnostic must quote the offending value (debug-escaped): {rendered}",
9127 );
9128 }
9129
9130 #[test]
9131 fn validate_etiquetas_rejects_at_21_byte_boundary() {
9132 // The 20-byte cap pin — boundary-exceeding case rejected,
9133 // boundary-accepting case passes. Mirrors the peer
9134 // `chart_keyword_shape_rejects_at_21_byte_boundary` substrate-
9135 // side pin, surfaced at the per-axis caller so the cap
9136 // propagates through validate end-to-end. Constructed as a
9137 // single all-`a` token so only the cap arm fires.
9138 let max_ok = "a".repeat(20);
9139 let c = caixa_with_etiquetas(vec![max_ok.as_str()]);
9140 c.validate_etiquetas().unwrap();
9141 let too_long = "a".repeat(21);
9142 let c = caixa_with_etiquetas(vec![too_long.as_str()]);
9143 let err = c.validate_etiquetas().unwrap_err();
9144 let ManifestError::EtiquetaInvalid { reason, .. } = err else {
9145 panic!("expected EtiquetaInvalid, got {err:?}");
9146 };
9147 assert!(reason.contains("20"), "got: {reason}");
9148 assert!(reason.contains("21"), "got: {reason}");
9149 }
9150
9151 #[test]
9152 fn validate_etiquetas_accepts_canonical_shaped_forms() {
9153 // Positive control sweep: every canonical-shaped tag from the
9154 // hello-rio / checkout-aplicacao / pangea-tatara-akeyless
9155 // example fixtures plus the substrate-fixed tags caixa-helm
9156 // unions in at chart render. Drift between this list and the
9157 // substrate-side `chart_keyword_shape_accepts_canonical_forms`
9158 // sweep surfaces here — one source of truth for the rule.
9159 let c = caixa_with_etiquetas(vec![
9160 "example",
9161 "aplicacao",
9162 "mesh",
9163 "ecommerce",
9164 "demo",
9165 "infrastructure",
9166 "aws",
9167 "akeyless",
9168 "pangea-native",
9169 "hello-world",
9170 "wasm",
9171 "rust",
9172 "tatara-lisp",
9173 "caixa-servico",
9174 "lareira",
9175 ]);
9176 c.validate_etiquetas().unwrap();
9177 }
9178
9179 // ── validate_autores — universal-axis maintainer shape ────────────
9180
9181 fn caixa_with_autores(autores: Vec<&str>) -> Caixa {
9182 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
9183 c.autores = autores.into_iter().map(String::from).collect();
9184 c
9185 }
9186
9187 #[test]
9188 fn validate_autores_accepts_empty_list() {
9189 // The empty-list identity: `Caixa::template` emits `:autores ()`,
9190 // so the gate is non-disruptive against every existing manifest.
9191 let c = caixa_with_autores(vec![]);
9192 c.validate_autores().unwrap();
9193 }
9194
9195 #[test]
9196 fn validate_autores_accepts_canonical_forms() {
9197 // Positive control sweep: every canonical-shaped non-empty
9198 // distinct maintainer list passes — the hello-rio / checkout-
9199 // aplicacao fixtures' `:autores ("pleme-io")` shape, plus the
9200 // multi-author shape downstream packaging surfaces emit.
9201 let c = caixa_with_autores(vec!["pleme-io"]);
9202 c.validate_autores().unwrap();
9203 let c = caixa_with_autores(vec!["alice <alice@example.com>", "bob <bob@example.com>"]);
9204 c.validate_autores().unwrap();
9205 }
9206
9207 #[test]
9208 fn validate_autores_rejects_empty_entry() {
9209 // Canonical paste-from-blank-doc footgun. Without the gate the
9210 // empty entry rendered as `maintainers: [{name: "", email: null}]`
9211 // in `Chart.yaml`, a no-op maintainer the substrate cannot route
9212 // to.
9213 let c = caixa_with_autores(vec![""]);
9214 let err = c.validate_autores().unwrap_err();
9215 assert!(matches!(err, ManifestError::AutorEmpty), "got {err:?}",);
9216 }
9217
9218 #[test]
9219 fn validate_autores_rejects_duplicate_entry() {
9220 // Canonical copy-paste-the-wrong-author footgun. Unlike the
9221 // `:etiquetas` peer (caixa-helm's `BTreeSet` collect silently
9222 // dedups the rendered `keywords:` array), the `maintainers:`
9223 // rendering has *no* dedup — duplicates stack verbatim. The
9224 // duplicate-arm names the offending author verbatim.
9225 let c = caixa_with_autores(vec!["pleme-io", "pleme-io"]);
9226 let err = c.validate_autores().unwrap_err();
9227 let ManifestError::AutorDuplicate { autor } = err else {
9228 panic!("expected AutorDuplicate, got {err:?}");
9229 };
9230 assert_eq!(autor, "pleme-io");
9231 }
9232
9233 #[test]
9234 fn validate_autores_empty_takes_precedence_over_duplicate() {
9235 // Empty-first cascade pin: `("" "pleme-io" "pleme-io")` surfaces
9236 // `AutorEmpty` not `AutorDuplicate` — the narrower structural
9237 // "this entry has no value" defect dominates the cross-entry
9238 // uniqueness diagnostic. Mirrors the peer empty-before-duplicate
9239 // cascades on `:etiquetas` (`EtiquetaEmpty` before
9240 // `EtiquetaDuplicate`, 360a499), `:caracteristicas`
9241 // (`CaracteristicaEmpty` before `CaracteristicaDuplicate`,
9242 // fc3b4d5), and `:membros :caixa` (`MembroCaixaEmpty` before
9243 // `MembroDuplicate`).
9244 let c = caixa_with_autores(vec!["", "pleme-io", "pleme-io"]);
9245 let err = c.validate_autores().unwrap_err();
9246 assert!(matches!(err, ManifestError::AutorEmpty), "got {err:?}",);
9247 }
9248
9249 #[test]
9250 fn validate_autores_duplicate_reports_first_collision() {
9251 // First-collision pin: `("a" "b" "a" "b")` surfaces the `"a"`
9252 // duplicate (the lexicographically-earliest offending position
9253 // — the second `"a"` at index 2 collides with the first `"a"`
9254 // at index 0), not the later `"b"` collision at index 3,
9255 // peer with every other first-collision diagnostic posture on
9256 // this surface.
9257 let c = caixa_with_autores(vec!["a", "b", "a", "b"]);
9258 let err = c.validate_autores().unwrap_err();
9259 let ManifestError::AutorDuplicate { autor } = err else {
9260 panic!("expected AutorDuplicate, got {err:?}");
9261 };
9262 assert_eq!(autor, "a");
9263 }
9264
9265 #[test]
9266 fn validate_autores_case_sensitive() {
9267 // Case-sensitivity pin: `("Pleme-io" "pleme-io")` is two distinct
9268 // entries, mirroring the peer `:etiquetas` / `:membros :caixa`
9269 // / `:children :caixa` exact-string-match discipline.
9270 let c = caixa_with_autores(vec!["Pleme-io", "pleme-io"]);
9271 c.validate_autores().unwrap();
9272 }
9273
9274 #[test]
9275 fn validate_autores_diagnostic_carries_offending_author() {
9276 // Diagnostic-shape pin (peer with
9277 // `validate_etiquetas_diagnostic_carries_offending_tag`): the
9278 // error's Display surfaces the offending author verbatim, so a
9279 // `feira lint` run can render the diagnostic without re-parsing
9280 // and the author can grep their caixa.lisp for the offending
9281 // value.
9282 let c = caixa_with_autores(vec!["pleme-io", "pleme-io"]);
9283 let rendered = c.validate_autores().unwrap_err().to_string();
9284 assert!(
9285 rendered.contains(":autores"),
9286 "diagnostic must name the offending slot: {rendered}",
9287 );
9288 assert!(
9289 rendered.contains("pleme-io"),
9290 "diagnostic must quote the offending author: {rendered}",
9291 );
9292 }
9293
9294 #[test]
9295 fn validate_autores_rejects_leading_whitespace_entry() {
9296 // Canonical paste-from-aligned-doc footgun. Without the shape
9297 // gate `" pleme-io"` silently passed validate and landed as a
9298 // YAML plain-style scalar with leading whitespace in the
9299 // rendered Chart.yaml `maintainers:` array — every YAML 1.2
9300 // dumper trims leading whitespace from plain-style scalars, so
9301 // the authored space round-tripped inconsistently back through
9302 // `caixa.lisp`. Mirrors the peer
9303 // `validate_descricao_rejects_leading_whitespace`.
9304 let c = caixa_with_autores(vec![" pleme-io"]);
9305 let err = c.validate_autores().unwrap_err();
9306 let ManifestError::AutorInvalid { autor, reason } = err else {
9307 panic!("expected AutorInvalid, got {err:?}");
9308 };
9309 assert_eq!(autor, " pleme-io");
9310 assert!(reason.contains("whitespace"), "got: {reason}");
9311 }
9312
9313 #[test]
9314 fn validate_autores_rejects_trailing_whitespace_entry() {
9315 // Canonical paste-from-doc footgun.
9316 let c = caixa_with_autores(vec!["pleme-io "]);
9317 let err = c.validate_autores().unwrap_err();
9318 let ManifestError::AutorInvalid { autor, reason } = err else {
9319 panic!("expected AutorInvalid, got {err:?}");
9320 };
9321 assert_eq!(autor, "pleme-io ");
9322 assert!(reason.contains("whitespace"), "got: {reason}");
9323 }
9324
9325 #[test]
9326 fn validate_autores_rejects_embedded_newline_entry() {
9327 // Canonical paste-from-multiline-doc footgun — the author
9328 // pasted a multi-line block of author records into one
9329 // `:autores` entry instead of splitting into one entry per
9330 // author. Without the shape gate `"alice\nbob"` silently
9331 // passed validate and landed as a YAML-illegal multi-line
9332 // scalar in the rendered Chart.yaml `maintainers:` array.
9333 let c = caixa_with_autores(vec!["alice\nbob"]);
9334 let err = c.validate_autores().unwrap_err();
9335 let ManifestError::AutorInvalid { autor, reason } = err else {
9336 panic!("expected AutorInvalid, got {err:?}");
9337 };
9338 assert_eq!(autor, "alice\nbob");
9339 assert!(reason.contains("newline"), "got: {reason}");
9340 }
9341
9342 #[test]
9343 fn validate_autores_rejects_embedded_carriage_return_entry() {
9344 // Canonical paste-from-Windows-CRLF-doc footgun.
9345 let c = caixa_with_autores(vec!["alice\rbob"]);
9346 let err = c.validate_autores().unwrap_err();
9347 let ManifestError::AutorInvalid { autor, reason } = err else {
9348 panic!("expected AutorInvalid, got {err:?}");
9349 };
9350 assert_eq!(autor, "alice\rbob");
9351 assert!(reason.contains("carriage return"), "got: {reason}");
9352 }
9353
9354 #[test]
9355 fn validate_autores_rejects_embedded_tab_entry() {
9356 // Canonical tab-from-aligned-doc footgun.
9357 let c = caixa_with_autores(vec!["Pleme\tContributors"]);
9358 let err = c.validate_autores().unwrap_err();
9359 let ManifestError::AutorInvalid { autor, reason } = err else {
9360 panic!("expected AutorInvalid, got {err:?}");
9361 };
9362 assert_eq!(autor, "Pleme\tContributors");
9363 assert!(reason.contains("tab"), "got: {reason}");
9364 }
9365
9366 #[test]
9367 fn validate_autores_rejects_embedded_control_bytes_entry() {
9368 // Paste-from-binary-blob footguns: NUL, BEL, ESC, DEL all
9369 // surface the same control-byte arm.
9370 for entry in [
9371 "alice\x00bob",
9372 "alice\x07bob",
9373 "alice\x1bbob",
9374 "alice\x7fbob",
9375 ] {
9376 let c = caixa_with_autores(vec![entry]);
9377 let err = c.validate_autores().unwrap_err();
9378 let ManifestError::AutorInvalid { autor, reason } = err else {
9379 panic!("expected AutorInvalid for {entry:?}, got {err:?}");
9380 };
9381 assert_eq!(autor, entry);
9382 assert!(
9383 reason.contains("control character"),
9384 "{entry:?} reason: {reason}",
9385 );
9386 }
9387 }
9388
9389 #[test]
9390 fn validate_autores_accepts_unicode_entry() {
9391 // Unicode positive control: realistic maintainer names carry
9392 // Unicode (`François`, `日本語`, `naïve`). The predicate must
9393 // round-trip Unicode losslessly, peer with the
9394 // `chart_maintainer_name_shape_accepts_unicode` substrate-side
9395 // sweep.
9396 let c = caixa_with_autores(vec![
9397 "François Dupont",
9398 "日本語の名前",
9399 "naïve <naive@example.com>",
9400 ]);
9401 c.validate_autores().unwrap();
9402 }
9403
9404 #[test]
9405 fn validate_autores_empty_takes_precedence_over_shape() {
9406 // Per-entry empty-first cascade pin: an entry that is both
9407 // empty *and* shape-invalid surfaces `AutorEmpty` (the narrower
9408 // "this entry has no value" structural defect dominates the
9409 // broader shape-predicate diagnostic). The empty arm fires
9410 // before the shape predicate is consulted, mirroring the peer
9411 // `validate_repositorio_empty_takes_precedence_over_shape`
9412 // cascade on the universal `Option<String>` siblings — and now
9413 // established on the Vec<String> per-entry surface.
9414 let c = caixa_with_autores(vec![""]);
9415 let err = c.validate_autores().unwrap_err();
9416 assert!(matches!(err, ManifestError::AutorEmpty), "got {err:?}",);
9417 }
9418
9419 #[test]
9420 fn validate_autores_shape_takes_precedence_over_duplicate() {
9421 // Per-entry shape-before-cross-entry-duplicate cascade pin: an
9422 // entry that is malformed surfaces `AutorInvalid` even when a
9423 // later entry would have collided on duplicate. The per-entry
9424 // shape arm fires inside the same loop iteration as the empty
9425 // arm, before the seen-set insert at end-of-iteration —
9426 // structural per-entry defects dominate the cross-entry
9427 // uniqueness diagnostic.
9428 let c = caixa_with_autores(vec!["alice\nbob", "alice\nbob"]);
9429 let err = c.validate_autores().unwrap_err();
9430 assert!(
9431 matches!(err, ManifestError::AutorInvalid { .. }),
9432 "got {err:?}",
9433 );
9434 }
9435
9436 #[test]
9437 fn validate_autores_invalid_diagnostic_names_offending_slot_and_value() {
9438 // Diagnostic-shape pin on the new shape arm (peer with
9439 // `validate_descricao_invalid_diagnostic_carries_offending_value`):
9440 // the rendered Display surfaces both the offending slot name
9441 // and the offending value verbatim, so a `feira lint` run
9442 // points the author at the exact `:autores` entry to fix.
9443 let c = caixa_with_autores(vec!["alice\nbob"]);
9444 let rendered = c.validate_autores().unwrap_err().to_string();
9445 assert!(
9446 rendered.contains(":autores"),
9447 "diagnostic must name the offending slot: {rendered}",
9448 );
9449 assert!(
9450 rendered.contains("alice\\nbob"),
9451 "diagnostic must quote the offending value (debug-escaped): {rendered}",
9452 );
9453 }
9454
9455 #[test]
9456 fn validate_autores_rejects_at_129_byte_boundary() {
9457 // The 128-byte cap pin — boundary-exceeding case rejected,
9458 // boundary-accepting case passes. Mirrors the peer
9459 // `chart_maintainer_name_shape_rejects_at_129_byte_boundary`
9460 // substrate-side pin, surfaced at the per-axis caller so the
9461 // cap propagates through validate end-to-end. Constructed as
9462 // a single all-`a` token so only the cap arm fires.
9463 let max_ok = "a".repeat(128);
9464 let c = caixa_with_autores(vec![max_ok.as_str()]);
9465 c.validate_autores().unwrap();
9466 let too_long = "a".repeat(129);
9467 let c = caixa_with_autores(vec![too_long.as_str()]);
9468 let err = c.validate_autores().unwrap_err();
9469 let ManifestError::AutorInvalid { reason, .. } = err else {
9470 panic!("expected AutorInvalid, got {err:?}");
9471 };
9472 assert!(reason.contains("128"), "got: {reason}");
9473 assert!(reason.contains("129"), "got: {reason}");
9474 }
9475
9476 // ── validate_repositorio — universal-axis git-repo-URL shape ──────
9477
9478 fn caixa_with_repositorio(repositorio: Option<&str>) -> Caixa {
9479 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
9480 c.repositorio = repositorio.map(String::from);
9481 c
9482 }
9483
9484 #[test]
9485 fn validate_repositorio_accepts_none() {
9486 // The omit-the-slot identity: `:repositorio` is optional. The
9487 // gate is a no-op when the author didn't declare a value —
9488 // every caixa without a `:repositorio` line trivially passes,
9489 // and the substrate-side renderers fall back to their
9490 // documented placeholder (`caixa-helm`'s `home: None`,
9491 // `caixa-flux`'s `https://github.com/pleme-io/<nome>` derived
9492 // URL). Mirrors the peer `validate_restart_window_accepts_none`
9493 // posture on the other `Option<String>` Caixa slot.
9494 let c = caixa_with_repositorio(None);
9495 c.validate_repositorio().unwrap();
9496 }
9497
9498 #[test]
9499 fn validate_repositorio_accepts_canonical_forms() {
9500 // Positive control sweep across every documented `:repositorio`
9501 // authoring shape — the same union the shared
9502 // `crate::render::is_git_repo_url` predicate accepts and the
9503 // peer `:deps :fonte :repo` axis already routes through.
9504 // Covers the `github:` shorthand (the canonical pleme-io
9505 // convention used in the `:repositorio` field of every
9506 // manifest fixture across `caixa-helm` / `caixa-mesh` and the
9507 // `examples/`), the `https://…` URL the README quickstart uses,
9508 // the `ssh://`, `git://`, `git@host:path` scp-style SSH, and
9509 // `file://` URL schemes the shared predicate documents.
9510 for repo in [
9511 "github:pleme-io/hello-rio",
9512 "github:pleme-io/checkout",
9513 "https://github.com/pleme-io/hello-rio",
9514 "ssh://git@github.com/pleme-io/hello-rio.git",
9515 "git://github.com/pleme-io/hello-rio.git",
9516 "git@github.com:pleme-io/hello-rio.git",
9517 "file:///srv/pleme/hello-rio",
9518 ] {
9519 let c = caixa_with_repositorio(Some(repo));
9520 c.validate_repositorio()
9521 .unwrap_or_else(|err| panic!("canonical {repo:?} must pass: {err:?}"));
9522 }
9523 }
9524
9525 #[test]
9526 fn validate_repositorio_rejects_empty_some() {
9527 // Canonical paste-from-blank-doc footgun. The narrower
9528 // [`ManifestError::RepositorioEmpty`] arm fires before the
9529 // shape predicate is consulted, mirroring the empty-first
9530 // cascade every peer per-axis identity gate uses
9531 // (`NomeEmpty` → `NomeInvalid`, `VersaoEmpty` → `VersaoInvalid`,
9532 // `FonteRepoEmpty` → `FonteRepoInvalid`). Without this gate
9533 // the empty `Some("")` silently passed the renderer's
9534 // `Option::unwrap_or_else(|| <fallback>)` (which only fires
9535 // on `None`) and landed as `home: ""` in `Chart.yaml` /
9536 // `url: ""` in the FluxCD `GitRepository`.
9537 let c = caixa_with_repositorio(Some(""));
9538 let err = c.validate_repositorio().unwrap_err();
9539 assert!(
9540 matches!(err, ManifestError::RepositorioEmpty),
9541 "got {err:?}",
9542 );
9543 }
9544
9545 #[test]
9546 fn validate_repositorio_rejects_whitespace() {
9547 // Paste-from-doc whitespace footgun. The shared
9548 // `is_git_repo_url` predicate refuses any whitespace byte; a
9549 // trailing space in a `:repositorio` value silently broke
9550 // `git clone '<value> '` at clone time. The diagnostic names
9551 // the offending value verbatim.
9552 let c = caixa_with_repositorio(Some("github:pleme-io/hello-rio "));
9553 let err = c.validate_repositorio().unwrap_err();
9554 let ManifestError::RepositorioInvalid { repositorio, .. } = err else {
9555 panic!("expected RepositorioInvalid, got {err:?}");
9556 };
9557 assert_eq!(repositorio, "github:pleme-io/hello-rio ");
9558 }
9559
9560 #[test]
9561 fn validate_repositorio_rejects_control_char() {
9562 // Paste-from-multiline-doc CRLF footgun — control characters
9563 // at the URL boundary are a class of subprocess-arg injection
9564 // and break git's URL parser at every porcelain entry point.
9565 let c = caixa_with_repositorio(Some("https://example.com/repo\n"));
9566 let err = c.validate_repositorio().unwrap_err();
9567 assert!(
9568 matches!(err, ManifestError::RepositorioInvalid { .. }),
9569 "got {err:?}",
9570 );
9571 }
9572
9573 #[test]
9574 fn validate_repositorio_rejects_leading_dash() {
9575 // Canonical CLI-argument-injection footgun: `git clone <repo>`
9576 // interprets a leading `-` as a CLI flag, so a
9577 // `-upload-pack=…` value escapes the subprocess argument
9578 // boundary. The shared predicate refuses every leading-`-`
9579 // shape at validate time.
9580 let c = caixa_with_repositorio(Some("-upload-pack=evil"));
9581 let err = c.validate_repositorio().unwrap_err();
9582 assert!(
9583 matches!(err, ManifestError::RepositorioInvalid { .. }),
9584 "got {err:?}",
9585 );
9586 }
9587
9588 #[test]
9589 fn validate_repositorio_rejects_missing_colon_separator() {
9590 // The bare `org/repo` ambiguity footgun — `git clone` reads
9591 // a no-`:` form as a relative filesystem path rather than the
9592 // GitHub-shorthand expansion the author probably intended.
9593 // The shared predicate refuses every shape without a `:`
9594 // separator.
9595 let c = caixa_with_repositorio(Some("pleme-io/hello-rio"));
9596 let err = c.validate_repositorio().unwrap_err();
9597 assert!(
9598 matches!(err, ManifestError::RepositorioInvalid { .. }),
9599 "got {err:?}",
9600 );
9601 }
9602
9603 #[test]
9604 fn validate_repositorio_rejects_fragment_anchor() {
9605 // Paste-from-browser-address-bar footgun on the
9606 // `:repositorio` axis — an author copies a GitHub permalink
9607 // to a README section / line-permalink and forgets to trim
9608 // the `#fragment` tail. The shared `is_git_repo_url`
9609 // predicate refuses the byte at the URL-grammar layer
9610 // (libcurl strips the fragment before opening the
9611 // transport, so the byte rides verbatim into the rendered
9612 // `Chart.yaml` `home:` and FluxCD `GitRepository` `url:`
9613 // fields but is silently dropped on the wire — two
9614 // manifest variants whose values differ only in their
9615 // fragment anchor lock to two distinct rendered artifacts
9616 // for the byte-identical clone, defeating the THEORY.md
9617 // §V.2 render-determinism contract on the `:repositorio`
9618 // axis the peer `:fonte :repo` axis already closes).
9619 let c = caixa_with_repositorio(Some("https://github.com/pleme-io/hello-rio#readme"));
9620 let err = c.validate_repositorio().unwrap_err();
9621 let ManifestError::RepositorioInvalid {
9622 repositorio,
9623 reason,
9624 } = err
9625 else {
9626 panic!("expected RepositorioInvalid, got {err:?}");
9627 };
9628 assert_eq!(repositorio, "https://github.com/pleme-io/hello-rio#readme");
9629 assert!(
9630 reason.contains("must not contain `#`"),
9631 "reason must surface the fragment-`#` arm, got {reason:?}"
9632 );
9633 }
9634
9635 #[test]
9636 fn validate_repositorio_rejects_query_string() {
9637 // Paste-from-browser-address-bar footgun on the
9638 // `:repositorio` axis (peer with the a68f818 fragment-`#`
9639 // arm on the same axis). An author copies a GitHub tab
9640 // deep-link out of the address bar and forgets to trim
9641 // the `?tab=…` query tail. The shared `is_git_repo_url`
9642 // predicate refuses the byte at the URL-grammar layer
9643 // (GitHub / GitLab / Bitbucket silently ignore the
9644 // `?query` tail and serve the same repo regardless, so
9645 // the byte rides verbatim into the rendered `Chart.yaml`
9646 // `home:` and FluxCD `GitRepository` `url:` fields but
9647 // is silently masked at the wire — two manifest variants
9648 // whose values differ only in their query tail lock to
9649 // two distinct rendered artifacts for the byte-identical
9650 // clone, defeating the THEORY.md §V.2 render-determinism
9651 // contract on the `:repositorio` axis the peer `:fonte
9652 // :repo` axis already closes).
9653 let c = caixa_with_repositorio(Some(
9654 "https://github.com/pleme-io/hello-rio?tab=readme-ov-file",
9655 ));
9656 let err = c.validate_repositorio().unwrap_err();
9657 let ManifestError::RepositorioInvalid {
9658 repositorio,
9659 reason,
9660 } = err
9661 else {
9662 panic!("expected RepositorioInvalid, got {err:?}");
9663 };
9664 assert_eq!(
9665 repositorio,
9666 "https://github.com/pleme-io/hello-rio?tab=readme-ov-file"
9667 );
9668 assert!(
9669 reason.contains("must not contain `?`"),
9670 "reason must surface the query-`?` arm, got {reason:?}"
9671 );
9672 }
9673
9674 #[test]
9675 fn validate_repositorio_rejects_embedded_backslash() {
9676 // Windows-file-path-confusion footgun on the `:repositorio`
9677 // axis (peer with the prior fragment-`#` / query-`?` arms on
9678 // the same axis, and peer with the new dep-level `:fonte :repo`
9679 // backslash arm on the URL-grammar trajectory). An author
9680 // pastes a Windows Explorer address-bar `file:///C:\Users\me\
9681 // hello-rio` into the `:repositorio` slot, expecting the
9682 // `lareira-<nome>` chart's `home:` field and the FluxCD
9683 // `GitRepository` `url:` field to render the canonical local
9684 // file-URI. The shared `is_git_repo_url` predicate refuses
9685 // the byte at the URL-grammar layer (libcurl silently
9686 // translates `\` → `/` on some platforms and refuses it on
9687 // others, so the byte rides verbatim into the rendered
9688 // artifacts but is silently rewritten or rejected at the wire
9689 // — two manifest variants whose values differ only in
9690 // backslash-vs-forward-slash lock to two distinct rendered
9691 // artifacts for the byte-identical clone, defeating the
9692 // THEORY.md §V.2 render-determinism contract on the
9693 // `:repositorio` axis the peer `:fonte :repo` axis already
9694 // closes).
9695 let c = caixa_with_repositorio(Some("file:///C:\\Users\\me\\hello-rio"));
9696 let err = c.validate_repositorio().unwrap_err();
9697 let ManifestError::RepositorioInvalid {
9698 repositorio,
9699 reason,
9700 } = err
9701 else {
9702 panic!("expected RepositorioInvalid, got {err:?}");
9703 };
9704 assert_eq!(repositorio, "file:///C:\\Users\\me\\hello-rio");
9705 assert!(
9706 reason.contains("must not contain `\\`"),
9707 "reason must surface the backslash-`\\` arm, got {reason:?}"
9708 );
9709 }
9710
9711 #[test]
9712 fn validate_repositorio_rejects_uri_template_placeholder() {
9713 // URI Template (RFC 6570) placeholder footgun on the
9714 // `:repositorio` axis (peer with the prior fragment-`#` /
9715 // query-`?` / backslash-`\` arms on the same axis, and peer
9716 // with the new dep-level `:fonte :repo` `{` / `}` arm on the
9717 // URL-grammar trajectory). An author pastes a quick-start
9718 // README snippet / OpenAPI `servers:` URL / Helm chart
9719 // `home:` template carrying unresolved `{org}` / `{repo}`
9720 // placeholders into the `:repositorio` slot, expecting the
9721 // substrate to resolve the placeholder downstream. The
9722 // shared `is_git_repo_url` predicate refuses the byte at the
9723 // URL-grammar layer (libcurl percent-encodes `{` / `}` to
9724 // `%7B` / `%7D` on the wire, so the byte round-trips
9725 // inconsistently between the rendered `Chart.yaml home:` /
9726 // FluxCD `GitRepository url:` and the resolver's `git clone`
9727 // invocation, defeating the THEORY.md §V.2 render-
9728 // determinism contract on the `:repositorio` axis the peer
9729 // `:fonte :repo` axis already closes; every git porcelain
9730 // entry-point additionally fetches a nonexistent literal-
9731 // `{placeholder}`-named path far from the source caixa.lisp).
9732 let c = caixa_with_repositorio(Some("https://github.com/{org}/hello-rio"));
9733 let err = c.validate_repositorio().unwrap_err();
9734 let ManifestError::RepositorioInvalid {
9735 repositorio,
9736 reason,
9737 } = err
9738 else {
9739 panic!("expected RepositorioInvalid, got {err:?}");
9740 };
9741 assert_eq!(repositorio, "https://github.com/{org}/hello-rio");
9742 assert!(
9743 reason.contains("must not contain `{`"),
9744 "reason must surface the open-brace `{{` arm, got {reason:?}"
9745 );
9746 assert!(
9747 reason.contains("URI Template") || reason.contains("RFC 6570"),
9748 "reason must name the RFC 6570 URI Template grammar, got {reason:?}"
9749 );
9750 }
9751
9752 #[test]
9753 fn validate_repositorio_empty_takes_precedence_over_shape() {
9754 // Empty-first cascade pin: the empty `Some("")` surfaces the
9755 // narrower `RepositorioEmpty` not the shape-predicate-wrapped
9756 // `RepositorioInvalid`, mirroring the peer
9757 // `NomeEmpty` → `NomeInvalid`, `VersaoEmpty` → `VersaoInvalid`,
9758 // `FonteRepoEmpty` → `FonteRepoInvalid` cascades. The shared
9759 // `is_git_repo_url` predicate also rejects the empty input
9760 // (defensively, with its own `"must not be empty"` reason),
9761 // but the manifest-layer empty arm runs first to surface the
9762 // narrower diagnostic verbatim.
9763 let c = caixa_with_repositorio(Some(""));
9764 let err = c.validate_repositorio().unwrap_err();
9765 assert!(
9766 matches!(err, ManifestError::RepositorioEmpty),
9767 "got {err:?}",
9768 );
9769 }
9770
9771 #[test]
9772 fn validate_repositorio_diagnostic_carries_offending_value() {
9773 // Diagnostic-shape pin (peer with
9774 // `validate_autores_diagnostic_carries_offending_author`): the
9775 // error's Display surfaces the offending value + slot name
9776 // verbatim, so a `feira lint` run can render the diagnostic
9777 // without re-parsing and the author can grep their caixa.lisp
9778 // for the offending `:repositorio` value.
9779 let c = caixa_with_repositorio(Some("pleme-io/hello-rio"));
9780 let rendered = c.validate_repositorio().unwrap_err().to_string();
9781 assert!(
9782 rendered.contains(":repositorio"),
9783 "diagnostic must name the offending slot: {rendered}",
9784 );
9785 assert!(
9786 rendered.contains("pleme-io/hello-rio"),
9787 "diagnostic must quote the offending value: {rendered}",
9788 );
9789 }
9790
9791 // ── validate_descricao — universal-axis Chart.yaml description shape ──
9792
9793 fn caixa_with_descricao(descricao: Option<&str>) -> Caixa {
9794 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
9795 c.descricao = descricao.map(String::from);
9796 c
9797 }
9798
9799 #[test]
9800 fn validate_descricao_accepts_none() {
9801 // The omit-the-slot identity: `:descricao` is optional. The
9802 // gate is a no-op when the author didn't declare a value —
9803 // every caixa without a `:descricao` line trivially passes,
9804 // and the substrate-side renderers fall back to their
9805 // documented `caixa.nome`-derived placeholder. Mirrors the
9806 // peer `validate_repositorio_accepts_none` posture on the
9807 // sibling `Option<String>` Caixa slot.
9808 let c = caixa_with_descricao(None);
9809 c.validate_descricao().unwrap();
9810 }
9811
9812 #[test]
9813 fn validate_descricao_accepts_canonical_summary() {
9814 // Positive control: the canonical pleme-io descricao shape —
9815 // a short free-form prose summary — passes the gate. Covers
9816 // the fixture shapes the `caixa-helm` / `caixa-flux` /
9817 // `caixa-mesh` test fixtures use (`"Canonical Rust→wasm32-
9818 // wasip2 caixa Servico."`, `"Checkout flow."`).
9819 for desc in [
9820 "Canonical Rust→wasm32-wasip2 caixa Servico.",
9821 "Checkout flow.",
9822 "AWS provider caixa for tatara-lisp",
9823 "FIXME — describe this caixa",
9824 "x",
9825 ] {
9826 let c = caixa_with_descricao(Some(desc));
9827 c.validate_descricao()
9828 .unwrap_or_else(|err| panic!("canonical {desc:?} must pass: {err:?}"));
9829 }
9830 }
9831
9832 #[test]
9833 fn validate_descricao_rejects_empty_some() {
9834 // Canonical paste-from-blank-doc footgun. Without this gate
9835 // the empty `Some("")` silently passed the renderer's
9836 // `Option::unwrap_or_else(|| <fallback>)` (which only fires
9837 // on `None`) and landed as `description: ""` in `Chart.yaml`
9838 // and a blank `README.md` header. Mirrors the peer
9839 // [`ManifestError::RepositorioEmpty`] empty-arm on the
9840 // sibling `Option<String>` Caixa slot.
9841 let c = caixa_with_descricao(Some(""));
9842 let err = c.validate_descricao().unwrap_err();
9843 assert!(matches!(err, ManifestError::DescricaoEmpty), "got {err:?}",);
9844 }
9845
9846 #[test]
9847 fn validate_descricao_rejects_leading_whitespace() {
9848 // Paste-from-aligned-doc footgun: a leading ASCII space the
9849 // bare empty-arm gate accepted, the shape predicate now
9850 // refuses. The diagnostic carries the offending value
9851 // verbatim (with the leading space preserved) so the author
9852 // can grep their caixa.lisp for the exact `:descricao` line
9853 // and fix the round-trip-inconsistent leading whitespace.
9854 // Mirrors the peer
9855 // `validate_licenca_rejects_leading_whitespace` arm on the
9856 // sibling `:licenca` axis.
9857 let c = caixa_with_descricao(Some(" Checkout flow."));
9858 let err = c.validate_descricao().unwrap_err();
9859 let ManifestError::DescricaoInvalid { descricao, reason } = err else {
9860 panic!("expected DescricaoInvalid, got {err:?}");
9861 };
9862 assert_eq!(descricao, " Checkout flow.");
9863 assert!(reason.contains("whitespace"), "got: {reason:?}");
9864 }
9865
9866 #[test]
9867 fn validate_descricao_rejects_trailing_whitespace() {
9868 // Paste-from-doc footgun: a trailing ASCII space the bare
9869 // empty-arm gate accepted, the shape predicate now refuses.
9870 let c = caixa_with_descricao(Some("Checkout flow. "));
9871 let err = c.validate_descricao().unwrap_err();
9872 let ManifestError::DescricaoInvalid { descricao, reason } = err else {
9873 panic!("expected DescricaoInvalid, got {err:?}");
9874 };
9875 assert_eq!(descricao, "Checkout flow. ");
9876 assert!(reason.contains("whitespace"), "got: {reason:?}");
9877 }
9878
9879 #[test]
9880 fn validate_descricao_rejects_embedded_newline() {
9881 // Paste-from-multiline-doc footgun: an embedded LF the bare
9882 // empty-arm gate accepted, the shape predicate now refuses.
9883 // Without this gate the embedded newline silently landed in
9884 // the rendered Chart.yaml as a multi-line YAML block scalar,
9885 // and every chart-aware UI (`helm list`, `helm search`,
9886 // Artifact Hub) renders the description in a single-line
9887 // column so the embedded newline is silently dropped at
9888 // every downstream consumer.
9889 let c = caixa_with_descricao(Some("Checkout\nflow."));
9890 let err = c.validate_descricao().unwrap_err();
9891 assert!(
9892 matches!(err, ManifestError::DescricaoInvalid { .. }),
9893 "got {err:?}",
9894 );
9895 assert!(err.to_string().contains("newline"), "got {err}");
9896 }
9897
9898 #[test]
9899 fn validate_descricao_rejects_embedded_carriage_return() {
9900 // Paste-from-Windows-CRLF-doc footgun.
9901 let c = caixa_with_descricao(Some("Checkout\rflow."));
9902 let err = c.validate_descricao().unwrap_err();
9903 assert!(
9904 matches!(err, ManifestError::DescricaoInvalid { .. }),
9905 "got {err:?}",
9906 );
9907 assert!(err.to_string().contains("carriage return"), "got {err}");
9908 }
9909
9910 #[test]
9911 fn validate_descricao_rejects_embedded_tab() {
9912 // Tab-from-aligned-doc footgun.
9913 let c = caixa_with_descricao(Some("Checkout\tflow."));
9914 let err = c.validate_descricao().unwrap_err();
9915 assert!(
9916 matches!(err, ManifestError::DescricaoInvalid { .. }),
9917 "got {err:?}",
9918 );
9919 assert!(err.to_string().contains("tab"), "got {err}");
9920 }
9921
9922 #[test]
9923 fn validate_descricao_rejects_embedded_control_bytes() {
9924 // Paste-from-binary-blob footgun: every other control byte
9925 // (NUL, BEL, ESC, DEL) is refused at validate time. Mirrors
9926 // the peer SPDX-expression control-byte arm.
9927 for s in [
9928 "Checkout\x00flow.",
9929 "Checkout\x07flow.",
9930 "Checkout\x1bflow.",
9931 "Checkout\x7fflow.",
9932 ] {
9933 let c = caixa_with_descricao(Some(s));
9934 let err = c.validate_descricao().unwrap_err();
9935 assert!(
9936 matches!(err, ManifestError::DescricaoInvalid { .. }),
9937 "{s:?} got {err:?}",
9938 );
9939 assert!(
9940 err.to_string().contains("control character"),
9941 "{s:?} got {err}",
9942 );
9943 }
9944 }
9945
9946 #[test]
9947 fn validate_descricao_accepts_unicode_prose() {
9948 // Positive control: Unicode prose is accepted — the
9949 // canonical fixtures carry `→` (U+2192) and `—` (U+2014),
9950 // and `Caixa::template`'s `"FIXME — describe this caixa"`
9951 // scaffold every `feira init` emits must continue to pass.
9952 for s in [
9953 "Canonical Rust→wasm32-wasip2 caixa Servico.",
9954 "FIXME — describe this caixa",
9955 "Caixa pour le projet tâche",
9956 "日本語の説明",
9957 ] {
9958 let c = caixa_with_descricao(Some(s));
9959 c.validate_descricao()
9960 .unwrap_or_else(|err| panic!("Unicode {s:?} must pass: {err:?}"));
9961 }
9962 }
9963
9964 #[test]
9965 fn validate_descricao_empty_takes_precedence_over_shape() {
9966 // Cascade pin: a `Some("")` surfaces the narrower
9967 // `DescricaoEmpty` arm, not the broader `DescricaoInvalid`
9968 // shape-predicate arm. Mirrors the peer
9969 // `validate_licenca_empty_takes_precedence_over_shape` pin
9970 // on the sibling `:licenca` axis.
9971 let c = caixa_with_descricao(Some(""));
9972 let err = c.validate_descricao().unwrap_err();
9973 assert!(matches!(err, ManifestError::DescricaoEmpty), "got {err:?}",);
9974 }
9975
9976 #[test]
9977 fn validate_descricao_invalid_diagnostic_carries_offending_value_and_slot() {
9978 // Diagnostic-shape pin: the error's Display surfaces both
9979 // the `:descricao` slot name and the offending value
9980 // verbatim, so a `feira lint` run can render the diagnostic
9981 // without re-parsing and the author can grep their caixa.lisp
9982 // for the offending `:descricao` line. Mirrors the peer
9983 // `validate_licenca_invalid_diagnostic_carries_offending_value_and_slot`
9984 // pin (ee2e888) on the sibling `:licenca` axis.
9985 // The `{descricao:?}` Debug format escapes embedded control
9986 // bytes; the quoted offending value surfaces as
9987 // `"Checkout\nflow."` (literal backslash-n) in the rendered
9988 // diagnostic. The author can grep their caixa.lisp for the
9989 // literal `Checkout` summary prefix.
9990 let c = caixa_with_descricao(Some("Checkout\nflow."));
9991 let rendered = c.validate_descricao().unwrap_err().to_string();
9992 assert!(
9993 rendered.contains(":descricao"),
9994 "diagnostic must name the offending slot: {rendered}",
9995 );
9996 assert!(
9997 rendered.contains("Checkout\\nflow."),
9998 "diagnostic must quote the offending value (debug-escaped): {rendered}",
9999 );
10000 }
10001
10002 #[test]
10003 fn validate_descricao_template_passes() {
10004 // Round-trip pin: the bare `Caixa::template` shape carries
10005 // `:descricao "FIXME — describe this caixa"` (a non-empty
10006 // sentinel), so the template-derived Caixa passes the gate by
10007 // construction. A future template-shape change that omits or
10008 // empties `:descricao` would surface here as a regression.
10009 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
10010 c.validate_descricao().unwrap();
10011 }
10012
10013 #[test]
10014 fn validate_descricao_diagnostic_names_offending_slot() {
10015 // Diagnostic-shape pin (peer with
10016 // `validate_repositorio_diagnostic_carries_offending_value`):
10017 // the error's Display surfaces the `:descricao` slot name
10018 // verbatim, so a `feira lint` run can render the diagnostic
10019 // without re-parsing and the author can grep their caixa.lisp
10020 // for the offending `:descricao` line.
10021 let c = caixa_with_descricao(Some(""));
10022 let rendered = c.validate_descricao().unwrap_err().to_string();
10023 assert!(
10024 rendered.contains(":descricao"),
10025 "diagnostic must name the offending slot: {rendered}",
10026 );
10027 }
10028
10029 // ── validate_licenca — universal-axis chart README license shape ──
10030
10031 fn caixa_with_licenca(licenca: Option<&str>) -> Caixa {
10032 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
10033 c.licenca = licenca.map(String::from);
10034 c
10035 }
10036
10037 #[test]
10038 fn validate_licenca_accepts_none() {
10039 // The omit-the-slot identity: `:licenca` is optional. The
10040 // gate is a no-op when the author didn't declare a value —
10041 // every caixa without a `:licenca` line trivially passes,
10042 // and the substrate-side `caixa-helm` renderer falls back to
10043 // the documented `"MIT"` placeholder. Mirrors the peer
10044 // `validate_descricao_accepts_none` posture on the sibling
10045 // `Option<String>` Caixa slot.
10046 let c = caixa_with_licenca(None);
10047 c.validate_licenca().unwrap();
10048 }
10049
10050 #[test]
10051 fn validate_licenca_accepts_canonical_expressions() {
10052 // Positive control: every canonical SPDX expression shape
10053 // pleme-io carries in its existing fixtures + the canonical
10054 // SPDX dual-license / with-exception / `+`-suffix / grouped /
10055 // user-defined-reference shapes all pass the gate. Covers
10056 // the single-license, `OR`-compound, `AND`-compound,
10057 // `WITH`-exception, parenthesis-grouped, `+`-suffix, and
10058 // `LicenseRef-` / `DocumentRef-:LicenseRef-` shapes — every
10059 // production the SPDX 2.1 expression grammar admits that
10060 // sits within the alphabet floor the
10061 // `is_spdx_expression_shape` predicate enforces.
10062 for lic in [
10063 "MIT",
10064 "Apache-2.0",
10065 "Apache-2.0 OR MIT",
10066 "Apache-2.0 AND MIT",
10067 "BSD-3-Clause",
10068 "MPL-2.0",
10069 "GPL-3.0-or-later",
10070 "GPL-2.0+",
10071 "Apache-2.0 WITH LLVM-exception",
10072 "(MIT OR Apache-2.0) AND BSD-3-Clause",
10073 "(MIT OR Apache-2.0) AND BSD-3-Clause AND ISC",
10074 "LicenseRef-MyLicense",
10075 "DocumentRef-spdx-tool:LicenseRef-MIT-Style",
10076 "x",
10077 ] {
10078 let c = caixa_with_licenca(Some(lic));
10079 c.validate_licenca()
10080 .unwrap_or_else(|err| panic!("canonical {lic:?} must pass: {err:?}"));
10081 }
10082 }
10083
10084 #[test]
10085 fn validate_licenca_rejects_trailing_whitespace() {
10086 // Paste-from-doc whitespace footgun. A trailing space in the
10087 // `:licenca` value would silently break a downstream SPDX
10088 // parser that splits on exact `AND` / `OR` / `WITH` keyword
10089 // boundaries. The shape predicate refuses every trailing
10090 // whitespace byte by construction. Peer with
10091 // `validate_repositorio_rejects_whitespace` and
10092 // `validate_edicao_rejects_trailing_whitespace`.
10093 let c = caixa_with_licenca(Some("MIT "));
10094 let err = c.validate_licenca().unwrap_err();
10095 let ManifestError::LicencaInvalid { licenca, .. } = err else {
10096 panic!("expected LicencaInvalid, got {err:?}");
10097 };
10098 assert_eq!(licenca, "MIT ");
10099 }
10100
10101 #[test]
10102 fn validate_licenca_rejects_leading_whitespace() {
10103 // Symmetric paste-from-doc whitespace footgun on the leading
10104 // boundary — the gate refuses every shape that starts with a
10105 // space byte by construction. Peer with
10106 // `validate_edicao_rejects_leading_whitespace`.
10107 let c = caixa_with_licenca(Some(" MIT"));
10108 let err = c.validate_licenca().unwrap_err();
10109 assert!(
10110 matches!(err, ManifestError::LicencaInvalid { .. }),
10111 "got {err:?}",
10112 );
10113 }
10114
10115 #[test]
10116 fn validate_licenca_rejects_control_char() {
10117 // Paste-from-multiline-doc CRLF footgun — control characters
10118 // at the value boundary land as a malformed line in the
10119 // rendered chart `README.md` `## License` section. Peer with
10120 // `validate_repositorio_rejects_control_char` and
10121 // `validate_edicao_rejects_control_char`.
10122 for lic in ["MIT\n", "MIT\r\n", "MIT\rApache-2.0"] {
10123 let c = caixa_with_licenca(Some(lic));
10124 let err = c.validate_licenca().unwrap_err();
10125 assert!(
10126 matches!(err, ManifestError::LicencaInvalid { .. }),
10127 "expected LicencaInvalid on {lic:?}, got {err:?}",
10128 );
10129 }
10130 }
10131
10132 #[test]
10133 fn validate_licenca_rejects_tab() {
10134 // Tab-from-aligned-doc footgun — SPDX expressions use a
10135 // single ASCII space between tokens; a tab breaks every
10136 // downstream SPDX parser that splits on exact `" "`
10137 // boundaries.
10138 let c = caixa_with_licenca(Some("MIT\tOR Apache-2.0"));
10139 let err = c.validate_licenca().unwrap_err();
10140 assert!(
10141 matches!(err, ManifestError::LicencaInvalid { .. }),
10142 "got {err:?}",
10143 );
10144 }
10145
10146 #[test]
10147 fn validate_licenca_rejects_non_ascii() {
10148 // Smart-quote / non-ASCII paste footgun — SPDX identifiers
10149 // are ASCII per the `idstring = 1*(ALPHA / DIGIT / "-" /
10150 // ".")` production. The shape predicate refuses every
10151 // non-ASCII byte by construction; peer with
10152 // `validate_edicao_rejects_non_ascii_lookalike`.
10153 for lic in ["MIT\u{a0}OR Apache-2.0", "MIT\u{2013}1.0", "Café-1.0"] {
10154 let c = caixa_with_licenca(Some(lic));
10155 let err = c.validate_licenca().unwrap_err();
10156 assert!(
10157 matches!(err, ManifestError::LicencaInvalid { .. }),
10158 "expected LicencaInvalid on {lic:?}, got {err:?}",
10159 );
10160 }
10161 }
10162
10163 #[test]
10164 fn validate_licenca_rejects_underscore() {
10165 // Underscore-instead-of-hyphen typo footgun — `Apache_2.0` /
10166 // `MIT_Style` / `BSD_3_Clause` are familiar shapes from
10167 // snake-case identifier conventions that don't apply to the
10168 // SPDX `idstring` grammar (which admits only `ALPHA / DIGIT /
10169 // "-" / "."`). The shape predicate refuses every underscore
10170 // byte by construction.
10171 for lic in ["Apache_2.0", "MIT_Style", "BSD_3_Clause"] {
10172 let c = caixa_with_licenca(Some(lic));
10173 let err = c.validate_licenca().unwrap_err();
10174 assert!(
10175 matches!(err, ManifestError::LicencaInvalid { .. }),
10176 "expected LicencaInvalid on {lic:?}, got {err:?}",
10177 );
10178 }
10179 }
10180
10181 #[test]
10182 fn validate_licenca_rejects_comma_separator() {
10183 // Comma-instead-of-`OR`-keyword colloquial idiom footgun —
10184 // SPDX expressions compose multiple licenses via `AND` / `OR`
10185 // keywords, not the comma separator. The shape predicate
10186 // refuses every comma byte by construction.
10187 for lic in ["MIT, Apache-2.0", "MIT,Apache-2.0"] {
10188 let c = caixa_with_licenca(Some(lic));
10189 let err = c.validate_licenca().unwrap_err();
10190 assert!(
10191 matches!(err, ManifestError::LicencaInvalid { .. }),
10192 "expected LicencaInvalid on {lic:?}, got {err:?}",
10193 );
10194 }
10195 }
10196
10197 #[test]
10198 fn validate_licenca_rejects_slash_dual_license() {
10199 // Slash-dual-license colloquial idiom footgun — the
10200 // `MIT/Apache-2.0` shape is common in Cargo's pre-SPDX
10201 // `package.license` field but non-SPDX; the SPDX equivalent
10202 // is `MIT OR Apache-2.0`. The shape predicate refuses every
10203 // forward-slash byte by construction.
10204 for lic in ["MIT/Apache-2.0", "MIT/BSD-3-Clause"] {
10205 let c = caixa_with_licenca(Some(lic));
10206 let err = c.validate_licenca().unwrap_err();
10207 assert!(
10208 matches!(err, ManifestError::LicencaInvalid { .. }),
10209 "expected LicencaInvalid on {lic:?}, got {err:?}",
10210 );
10211 }
10212 }
10213
10214 #[test]
10215 fn validate_licenca_rejects_semicolon_separator() {
10216 // Semicolon-list-separator confusion footgun — adjacent to
10217 // the comma-separator idiom, every list-separator-belongs-
10218 // to-list-grammar confusion lands here.
10219 let c = caixa_with_licenca(Some("MIT; Apache-2.0"));
10220 let err = c.validate_licenca().unwrap_err();
10221 assert!(
10222 matches!(err, ManifestError::LicencaInvalid { .. }),
10223 "got {err:?}",
10224 );
10225 }
10226
10227 #[test]
10228 fn validate_licenca_empty_takes_precedence_over_shape() {
10229 // Empty-first cascade pin: the empty `Some("")` surfaces the
10230 // narrower `LicencaEmpty` not the shape-predicate-wrapped
10231 // `LicencaInvalid`, mirroring the peer
10232 // `validate_edicao_empty_takes_precedence_over_shape` and
10233 // `validate_repositorio_empty_takes_precedence_over_shape`
10234 // (`RepositorioEmpty` → `RepositorioInvalid`), `NomeEmpty` →
10235 // `NomeInvalid`, `VersaoEmpty` → `VersaoInvalid` cascades.
10236 // The shape predicate also refuses the empty input
10237 // (defensively — `"must not be empty"`), but the manifest-
10238 // layer empty arm runs first to surface the narrower
10239 // diagnostic verbatim.
10240 let c = caixa_with_licenca(Some(""));
10241 let err = c.validate_licenca().unwrap_err();
10242 assert!(matches!(err, ManifestError::LicencaEmpty), "got {err:?}",);
10243 }
10244
10245 #[test]
10246 fn validate_licenca_invalid_diagnostic_carries_offending_value() {
10247 // Diagnostic-shape pin on the shape-predicate arm (peer with
10248 // `validate_edicao_invalid_diagnostic_carries_offending_value`
10249 // and `validate_repositorio_diagnostic_carries_offending_value`):
10250 // the error's Display surfaces the offending value + slot
10251 // name verbatim, so a `feira lint` run can render the
10252 // diagnostic without re-parsing and the author can grep
10253 // their caixa.lisp for the offending `:licenca` value.
10254 let c = caixa_with_licenca(Some("Apache_2.0"));
10255 let rendered = c.validate_licenca().unwrap_err().to_string();
10256 assert!(
10257 rendered.contains(":licenca"),
10258 "diagnostic must name the offending slot: {rendered}",
10259 );
10260 assert!(
10261 rendered.contains("Apache_2.0"),
10262 "diagnostic must quote the offending value: {rendered}",
10263 );
10264 }
10265
10266 #[test]
10267 fn validate_licenca_rejects_empty_some() {
10268 // Canonical paste-from-blank-doc footgun. Without this gate
10269 // the empty `Some("")` silently passed the renderer's
10270 // `Option::unwrap_or_else(|| "MIT".into())` (which only
10271 // fires on `None`) and landed as a bare trailing period in
10272 // the rendered chart `README.md` `## License` section.
10273 // Mirrors the peer [`ManifestError::DescricaoEmpty`] empty-
10274 // arm on the sibling `Option<String>` Caixa slot.
10275 let c = caixa_with_licenca(Some(""));
10276 let err = c.validate_licenca().unwrap_err();
10277 assert!(matches!(err, ManifestError::LicencaEmpty), "got {err:?}",);
10278 }
10279
10280 #[test]
10281 fn validate_licenca_template_passes() {
10282 // Round-trip pin: the bare `Caixa::template` shape (whether
10283 // it carries `:licenca` or omits it) passes the gate by
10284 // construction. A future template-shape change that
10285 // introduced `(:licenca "")` would surface here as a
10286 // regression. Mirrors the peer
10287 // `validate_descricao_template_passes` pin.
10288 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
10289 c.validate_licenca().unwrap();
10290 }
10291
10292 #[test]
10293 fn validate_licenca_diagnostic_names_offending_slot() {
10294 // Diagnostic-shape pin (peer with
10295 // `validate_descricao_diagnostic_names_offending_slot`):
10296 // the error's Display surfaces the `:licenca` slot name
10297 // verbatim, so a `feira lint` run can render the diagnostic
10298 // without re-parsing and the author can grep their caixa.lisp
10299 // for the offending `:licenca` line.
10300 let c = caixa_with_licenca(Some(""));
10301 let rendered = c.validate_licenca().unwrap_err().to_string();
10302 assert!(
10303 rendered.contains(":licenca"),
10304 "diagnostic must name the offending slot: {rendered}",
10305 );
10306 }
10307
10308 // ── Caixa::licenca — outer top-level Option<&str> scalar accessor ──
10309
10310 #[test]
10311 fn licenca_returns_licenca_byte_string_verbatim_across_permutations() {
10312 // The canonical per-`Caixa` `:licenca` SPDX-expression scalar
10313 // pin: [`Caixa::licenca`] must return the `:licenca` typed
10314 // byte-string verbatim as an `Option<&str>`, byte-equal to the
10315 // raw `self.licenca.as_deref()` access across every
10316 // representative value in the accept-set — `None` (the "omit
10317 // the slot to defer to the caixa-helm renderer's `MIT`
10318 // fallback" arm every existing fixture without a `:licenca`
10319 // line carries), `Some("")` (a past-the-guard sentinel that
10320 // pins the accessor doesn't perform a silent
10321 // `Some("") → None` collapse on the empty arm — validate
10322 // rejects `Some("")` through `LicencaEmpty` but the accessor
10323 // must ship the raw slot verbatim so a validate-time gate
10324 // regression surfaces at the caixa-helm emit boundary rather
10325 // than being silently absorbed into the fallback), `Some("MIT")`
10326 // (the canonical single-license shape every `feira init`
10327 // template scaffolds), `Some("Apache-2.0 OR MIT")` (the
10328 // canonical `OR`-compound shape the peer
10329 // `validate_licenca_accepts_canonical_expressions` positive
10330 // sweep exercises), `Some("(MIT OR Apache-2.0) AND
10331 // BSD-3-Clause")` (the canonical parenthesis-grouped shape),
10332 // `Some("MIT ")` / `Some(" MIT")` / `Some("MIT\n")` /
10333 // `Some("Apache_2.0")` / `Some("MIT,Apache-2.0")` (past-the-
10334 // guard sentinels — validate rejects each through
10335 // `LicencaInvalid` but the accessor must ship the raw slot
10336 // verbatim).
10337 //
10338 // First outer top-level [`Caixa`] `Option<&str>`-return scalar
10339 // accessor pin on the substrate primitive — opens the "outer
10340 // [`Caixa`] `Option<&str>` scalar" projection pattern the
10341 // sibling per-`Caixa` `:descricao` / `:repositorio` / `:edicao`
10342 // future lifts fold on. Sibling in shape to the peer per-`:placement`
10343 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
10344 // [`crate::aplicacao::Placement::affinity`] (74ec2d3) accessor
10345 // pins on the sibling per-M3-mesh-slot `Option<&str>`-return
10346 // axes, extended onto the outer top-level [`Caixa`] universal-
10347 // axis surface. Pins against a future silent detour that
10348 // returned an owned `Option<String>` (which would type-check
10349 // but silently allocate on every accessor call, breaking the
10350 // zero-cost projection every peer sibling accessor carries), a
10351 // `Some("") → None` collapse (which would silently absorb the
10352 // `LicencaEmpty` refusal case at the accessor boundary and the
10353 // caixa-helm emit path would silently fall back to `"MIT"` on
10354 // a struct-literal `Caixa { licenca: Some(""), .. }`), or a
10355 // `None → Some("MIT")` collapse (which would silently reify
10356 // the caixa-helm renderer's `"MIT"` fallback at the accessor
10357 // boundary and every downstream consumer keying off the
10358 // `Option::is_none()` discriminator would lose the "author
10359 // omitted the slot" signal).
10360 for licenca in [
10361 None,
10362 Some(""),
10363 Some("MIT"),
10364 Some("Apache-2.0 OR MIT"),
10365 Some("(MIT OR Apache-2.0) AND BSD-3-Clause"),
10366 Some("MIT "),
10367 Some(" MIT"),
10368 Some("MIT\n"),
10369 Some("Apache_2.0"),
10370 Some("MIT,Apache-2.0"),
10371 ] {
10372 let c = caixa_with_licenca(licenca);
10373 assert_eq!(
10374 c.licenca(),
10375 licenca,
10376 "Caixa::licenca must return :licenca verbatim (got {:?}, \
10377 expected {licenca:?})",
10378 c.licenca(),
10379 );
10380 assert_eq!(
10381 c.licenca(),
10382 c.licenca.as_deref(),
10383 "Caixa::licenca must byte-equal the raw \
10384 `self.licenca.as_deref()` field access across every \
10385 value in the Option<&str> accept-set",
10386 );
10387 }
10388 }
10389
10390 #[test]
10391 fn validate_licenca_empty_arm_routes_through_accessor() {
10392 // Composition pin: [`Caixa::validate_licenca`]'s empty-arm gate
10393 // must key off [`Caixa::licenca`], not the raw
10394 // `self.licenca.as_deref()` field access. Structurally: a
10395 // `Caixa { licenca: Some(""), .. }` must surface the
10396 // `LicencaEmpty` refusal exactly, and a
10397 // `Caixa { licenca: Some("MIT"), .. }` (the canonical
10398 // single-license form) must pass validate. The pair jointly
10399 // pins the accessor + validate-gate composition: any future
10400 // silent detour that had the accessor return `None` on the
10401 // empty arm (a `.filter(|s| !s.is_empty())` collapse) would
10402 // silently absorb the `LicencaEmpty` refusal at the accessor
10403 // boundary and the validate gate would accept a struct-literal
10404 // `Caixa { licenca: Some(""), .. }` — the composition pin
10405 // catches that at caixa-core build time.
10406 //
10407 // Peer of the per-`:politicas :circuit-breaker`
10408 // [`crate::aplicacao::CircuitBreaker::max_failures`] (3a74062)
10409 // accessor-composition pin
10410 // (`validate_politicas_max_failures_zero_floor_arm_routes_through_accessor`)
10411 // on the sibling per-M3-mesh-slot required-`u32` axis — same
10412 // "the validate / shape-gate predicate must route through the
10413 // substrate-primitive typed dispatch" discipline extended onto
10414 // the outer top-level [`Caixa`] universal-axis
10415 // `Option<&str>`-composition surface.
10416 let c = caixa_with_licenca(Some(""));
10417 assert!(
10418 matches!(c.validate_licenca(), Err(ManifestError::LicencaEmpty)),
10419 "validate_licenca must reject licenca == Some(\"\") with \
10420 LicencaEmpty — the accessor and the validate gate must \
10421 route through the same substrate-primitive typed dispatch \
10422 on the :licenca empty arm",
10423 );
10424 let c = caixa_with_licenca(Some("MIT"));
10425 assert!(
10426 c.validate_licenca().is_ok(),
10427 "validate_licenca must accept licenca == Some(\"MIT\") \
10428 (the canonical single-license SPDX shape)",
10429 );
10430 }
10431
10432 #[test]
10433 fn licenca_projects_option_str_by_borrow() {
10434 // The by-borrow pin: [`Caixa::licenca`] returns
10435 // `Option<&str>` by borrow — the `&str` borrows the underlying
10436 // `String` storage of the `Option<String>` slot and the
10437 // accessor must not allocate a fresh `String` on every call.
10438 // Peer of the per-`:placement`
10439 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) by-
10440 // borrow pin on the peer per-M3-mesh-slot
10441 // `Option<&str>`-return axis, extended onto the outer top-
10442 // level [`Caixa`] universal-axis `Option<&str>` shape — the
10443 // accessor's returned `&str` must borrow from `&self` (the
10444 // returned reference's lifetime is tied to `&self`), and
10445 // calling the accessor twice on the same [`Caixa`] must yield
10446 // the same `Option<&str>` verbatim (idempotent, no side
10447 // effects on `&self`).
10448 //
10449 // Pins against a future silent detour that returned an owned
10450 // `Option<String>` (which would type-check but silently
10451 // allocate on every call, breaking the zero-cost projection
10452 // every peer sibling accessor carries), or a one-arm-only
10453 // accessor that returned a saturating value on some sentinel
10454 // input (breaking the pass-through invariant the sibling
10455 // required-scalar accessors carry).
10456 for licenca in [None, Some(""), Some("MIT"), Some("Apache-2.0 OR MIT")] {
10457 let c = caixa_with_licenca(licenca);
10458 let first = c.licenca();
10459 let second = c.licenca();
10460 assert_eq!(
10461 first, second,
10462 "Caixa::licenca must be idempotent — two successive \
10463 calls on the same &self must return the same \
10464 Option<&str>",
10465 );
10466 assert_eq!(
10467 first, licenca,
10468 "Caixa::licenca must return :licenca verbatim by \
10469 borrow — got {first:?}, expected {licenca:?}",
10470 );
10471 }
10472 }
10473
10474 // ── Caixa::repositorio — outer top-level Option<&str> scalar accessor ──
10475
10476 #[test]
10477 fn repositorio_returns_repositorio_byte_string_verbatim_across_permutations() {
10478 // The canonical per-`Caixa` `:repositorio` git-repo-URL scalar
10479 // pin: [`Caixa::repositorio`] must return the `:repositorio`
10480 // typed byte-string verbatim as an `Option<&str>`, byte-equal
10481 // to the raw `self.repositorio.as_deref()` access across every
10482 // representative value in the accept-set — `None` (the "omit
10483 // the slot to defer to the per-renderer placeholder" arm every
10484 // existing fixture without a `:repositorio` line carries),
10485 // `Some("")` (a past-the-guard sentinel that pins the accessor
10486 // doesn't perform a silent `Some("") → None` collapse on the
10487 // empty arm — validate rejects `Some("")` through
10488 // `RepositorioEmpty` but the accessor must ship the raw slot
10489 // verbatim so a validate-time gate regression surfaces at the
10490 // caixa-helm / caixa-flux emit boundary rather than being
10491 // silently absorbed into the per-renderer fallback),
10492 // `Some("github:pleme-io/hello-rio")` (the canonical `github:`
10493 // shorthand every existing manifest fixture across
10494 // `caixa-helm` / `caixa-mesh` and the `examples/` uses),
10495 // `Some("https://github.com/pleme-io/checkout")` (the canonical
10496 // `https://` URL the README quickstart uses),
10497 // `Some("ssh://git@github.com/pleme-io/checkout.git")` /
10498 // `Some("git://github.com/pleme-io/checkout.git")` /
10499 // `Some("git@github.com:pleme-io/checkout.git")` /
10500 // `Some("file:///opt/mirrors/pleme-io/checkout")` (every non-
10501 // github scheme the shared `is_git_repo_url` predicate
10502 // documents), and five past-the-guard sentinels for the
10503 // `RepositorioInvalid` refusal cases (`Some("pleme-io/checkout")`
10504 // missing-colon, `Some("-upload-pack=evil")` leading-dash, /
10505 // `Some("github:pleme-io/checkout?ref=main")` query-string, /
10506 // `Some("github:pleme-io/checkout#main")` fragment-anchor, /
10507 // `Some("github:pleme-io/{tpl}")` URI-template-placeholder — the
10508 // sentinels pin the accessor doesn't silently absorb the
10509 // refusal cases into a fallback).
10510 //
10511 // Second outer top-level [`Caixa`] `Option<&str>`-return scalar
10512 // accessor pin on the substrate primitive — sibling of the peer
10513 // [`Caixa::licenca`] (6d5bc28) pin
10514 // (`licenca_returns_licenca_byte_string_verbatim_across_permutations`)
10515 // that opened the "outer [`Caixa`] `Option<&str>` scalar"
10516 // projection pin pattern this pin folds on. Sibling in shape to
10517 // the peer per-`:placement`
10518 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
10519 // [`crate::aplicacao::Placement::affinity`] (74ec2d3) accessor
10520 // pins on the sibling per-M3-mesh-slot `Option<&str>`-return
10521 // axes, extended onto the outer top-level [`Caixa`] universal-
10522 // axis surface. Pins against a future silent detour that
10523 // returned an owned `Option<String>` (which would type-check
10524 // but silently allocate on every accessor call, breaking the
10525 // zero-cost projection every peer sibling accessor carries), a
10526 // `Some("") → None` collapse (which would silently absorb the
10527 // `RepositorioEmpty` refusal case at the accessor boundary and
10528 // the caixa-helm `Chart.yaml` `home:` fold would silently
10529 // render a `home: null` / omitted field on a struct-literal
10530 // `Caixa { repositorio: Some(""), .. }`), or a
10531 // `None → Some(<default>)` collapse (which would silently reify
10532 // the per-renderer fallback at the accessor boundary and every
10533 // downstream consumer keying off the `Option::is_none()`
10534 // discriminator would lose the "author omitted the slot"
10535 // signal).
10536 for repositorio in [
10537 None,
10538 Some(""),
10539 Some("github:pleme-io/hello-rio"),
10540 Some("https://github.com/pleme-io/checkout"),
10541 Some("ssh://git@github.com/pleme-io/checkout.git"),
10542 Some("git://github.com/pleme-io/checkout.git"),
10543 Some("git@github.com:pleme-io/checkout.git"),
10544 Some("file:///opt/mirrors/pleme-io/checkout"),
10545 Some("pleme-io/checkout"),
10546 Some("-upload-pack=evil"),
10547 Some("github:pleme-io/checkout?ref=main"),
10548 Some("github:pleme-io/checkout#main"),
10549 Some("github:pleme-io/{tpl}"),
10550 ] {
10551 let c = caixa_with_repositorio(repositorio);
10552 assert_eq!(
10553 c.repositorio(),
10554 repositorio,
10555 "Caixa::repositorio must return :repositorio verbatim \
10556 (got {:?}, expected {repositorio:?})",
10557 c.repositorio(),
10558 );
10559 assert_eq!(
10560 c.repositorio(),
10561 c.repositorio.as_deref(),
10562 "Caixa::repositorio must byte-equal the raw \
10563 `self.repositorio.as_deref()` field access across every \
10564 value in the Option<&str> accept-set",
10565 );
10566 }
10567 }
10568
10569 #[test]
10570 fn validate_repositorio_empty_arm_routes_through_accessor() {
10571 // Composition pin: [`Caixa::validate_repositorio`]'s empty-arm
10572 // gate must key off [`Caixa::repositorio`], not the raw
10573 // `self.repositorio.as_deref()` field access. Structurally: a
10574 // `Caixa { repositorio: Some(""), .. }` must surface the
10575 // `RepositorioEmpty` refusal exactly, and a
10576 // `Caixa { repositorio: Some("github:pleme-io/hello-rio"), .. }`
10577 // (the canonical `github:` shorthand form) must pass validate.
10578 // The pair jointly pins the accessor + validate-gate
10579 // composition: any future silent detour that had the accessor
10580 // return `None` on the empty arm (a `.filter(|s| !s.is_empty())`
10581 // collapse) would silently absorb the `RepositorioEmpty` refusal
10582 // at the accessor boundary and the validate gate would accept a
10583 // struct-literal `Caixa { repositorio: Some(""), .. }` — the
10584 // composition pin catches that at caixa-core build time.
10585 //
10586 // Peer of the [`Caixa::licenca`] (6d5bc28)
10587 // `validate_licenca_empty_arm_routes_through_accessor`
10588 // composition pin on the sibling outer top-level [`Caixa`]
10589 // `Option<&str>` universal-axis surface — same "the validate /
10590 // shape-gate predicate must route through the substrate-
10591 // primitive typed dispatch" discipline extended onto the second
10592 // outer top-level [`Caixa`] universal-axis `Option<&str>`-
10593 // composition surface.
10594 let c = caixa_with_repositorio(Some(""));
10595 assert!(
10596 matches!(
10597 c.validate_repositorio(),
10598 Err(ManifestError::RepositorioEmpty),
10599 ),
10600 "validate_repositorio must reject repositorio == Some(\"\") \
10601 with RepositorioEmpty — the accessor and the validate gate \
10602 must route through the same substrate-primitive typed \
10603 dispatch on the :repositorio empty arm",
10604 );
10605 let c = caixa_with_repositorio(Some("github:pleme-io/hello-rio"));
10606 assert!(
10607 c.validate_repositorio().is_ok(),
10608 "validate_repositorio must accept repositorio == \
10609 Some(\"github:pleme-io/hello-rio\") (the canonical \
10610 `github:` shorthand git-repo-URL shape)",
10611 );
10612 }
10613
10614 #[test]
10615 fn repositorio_projects_option_str_by_borrow() {
10616 // The by-borrow pin: [`Caixa::repositorio`] returns
10617 // `Option<&str>` by borrow — the `&str` borrows the underlying
10618 // `String` storage of the `Option<String>` slot and the
10619 // accessor must not allocate a fresh `String` on every call.
10620 // Peer of the per-`:placement`
10621 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) and the
10622 // [`Caixa::licenca`] (6d5bc28) by-borrow pins on the peer
10623 // `Option<&str>`-return axes, extended onto the second outer
10624 // top-level [`Caixa`] universal-axis `Option<&str>` shape —
10625 // the accessor's returned `&str` must borrow from `&self` (the
10626 // returned reference's lifetime is tied to `&self`), and
10627 // calling the accessor twice on the same [`Caixa`] must yield
10628 // the same `Option<&str>` verbatim (idempotent, no side effects
10629 // on `&self`).
10630 //
10631 // Pins against a future silent detour that returned an owned
10632 // `Option<String>` (which would type-check but silently
10633 // allocate on every call, breaking the zero-cost projection
10634 // every peer sibling accessor carries), or a one-arm-only
10635 // accessor that returned a saturating value on some sentinel
10636 // input (breaking the pass-through invariant the sibling
10637 // required-scalar accessors carry).
10638 for repositorio in [
10639 None,
10640 Some(""),
10641 Some("github:pleme-io/hello-rio"),
10642 Some("https://github.com/pleme-io/checkout"),
10643 ] {
10644 let c = caixa_with_repositorio(repositorio);
10645 let first = c.repositorio();
10646 let second = c.repositorio();
10647 assert_eq!(
10648 first, second,
10649 "Caixa::repositorio must be idempotent — two successive \
10650 calls on the same &self must return the same \
10651 Option<&str>",
10652 );
10653 assert_eq!(
10654 first, repositorio,
10655 "Caixa::repositorio must return :repositorio verbatim by \
10656 borrow — got {first:?}, expected {repositorio:?}",
10657 );
10658 }
10659 }
10660
10661 // ── Caixa::canonical_git_url — resolved-git-URL composer ──────────
10662
10663 #[test]
10664 fn canonical_git_url_returns_repositorio_verbatim_on_some_arm() {
10665 // Fail-before-pass-after pin: [`Caixa::canonical_git_url`] must
10666 // return the author-declared `:repositorio` byte-string verbatim
10667 // on the `Some` arm — no scheme rewrite, no trailing-slash
10668 // canonicalization, no `github:` → `https://github.com/`
10669 // desugaring. The resolved-URL composer is the projection of
10670 // the raw [`Caixa::repositorio`] `Option<&str>` accessor onto
10671 // the `String`-return arity every substrate-side field-fill
10672 // consumer keys off; on the `Some` arm the projection is
10673 // `str::to_owned` verbatim, so every accept-set value the
10674 // sibling `repositorio_returns_repositorio_byte_string_verbatim_
10675 // across_permutations` pin covers (`https://…`, `github:…`,
10676 // `ssh://…`, `git://…`, `git@…`, `file://…`, and the past-the-
10677 // guard sentinel `pleme-io/…`) must survive the accessor
10678 // byte-equal. Pins against a future silent detour that rewrote
10679 // the `github:` shorthand to the `https://github.com/` full URL
10680 // at the accessor boundary (which would silently split the
10681 // resolved-URL surface from the raw [`Caixa::repositorio`]
10682 // accessor's documented pass-through invariant), or a trailing-
10683 // slash normalization (which would silently break the
10684 // FluxCD `GitRepository` `spec.url` byte-exact match every
10685 // downstream consumer keys the source-controller reconcile off).
10686 for repositorio in [
10687 "github:pleme-io/hello-rio",
10688 "https://github.com/pleme-io/checkout",
10689 "ssh://git@github.com/pleme-io/checkout.git",
10690 "git://github.com/pleme-io/checkout.git",
10691 "git@github.com:pleme-io/checkout.git",
10692 "file:///opt/mirrors/pleme-io/checkout",
10693 ] {
10694 let c = caixa_with_repositorio(Some(repositorio));
10695 assert_eq!(
10696 c.canonical_git_url(),
10697 repositorio,
10698 "Caixa::canonical_git_url on the Some arm must return \
10699 :repositorio verbatim (got {:?}, expected {repositorio:?})",
10700 c.canonical_git_url(),
10701 );
10702 }
10703 }
10704
10705 #[test]
10706 fn canonical_git_url_falls_back_to_pleme_org_url_on_none_arm() {
10707 // Fail-before-pass-after pin: [`Caixa::canonical_git_url`] on the
10708 // `None` arm must emit the substrate's canonical pleme-org github
10709 // URL derived from `caixa.nome()` — `https://github.com/<org>/
10710 // <nome>` with `<org>` bound to [`crate::DEFAULT_PLEME_GIT_ORG`]
10711 // and `<nome>` bound to the typed [`Caixa::nome`] accessor. This
10712 // is the exact byte-image of the prior inline
10713 // [`caixa-flux::ClusterBundleOpts::for_caixa`] `git_url`
10714 // composer at caixa-flux/src/lib.rs:2080 that every prior caller
10715 // re-derived open-coded. Pins against a future silent detour
10716 // that migrated the `<org>` segment to a different constant (a
10717 // fork rebranding that split off a new
10718 // `DEFAULT_PLEME_GIT_ORG_MIRROR` const the accessor would need
10719 // to migrate onto), a scheme change (`https://` → `git://` or
10720 // `ssh://`), or a per-`Caixa` `.canonical_git_url_prefix`
10721 // override (which would break the substrate-wide single-source-
10722 // of-truth guarantee this method encodes).
10723 let c = caixa_with_repositorio(None);
10724 let expected = format!(
10725 "https://github.com/{org}/{nome}",
10726 org = crate::DEFAULT_PLEME_GIT_ORG,
10727 nome = c.nome(),
10728 );
10729 assert_eq!(
10730 c.canonical_git_url(),
10731 expected,
10732 "Caixa::canonical_git_url on the None arm must fold through \
10733 the substrate's canonical pleme-org github URL fallback \
10734 `https://github.com/<DEFAULT_PLEME_GIT_ORG>/<nome>` — got \
10735 {:?}, expected {expected:?}",
10736 c.canonical_git_url(),
10737 );
10738 }
10739
10740 #[test]
10741 fn canonical_git_url_byte_matches_manual_composition() {
10742 // Byte-parity pin: [`Caixa::canonical_git_url`] must render
10743 // byte-identically to the manual open-coded
10744 // `caixa.repositorio().map(str::to_owned).unwrap_or_else(||
10745 // format!("https://github.com/{org}/{nome}", ...))` composition
10746 // every prior substrate-side caller re-derived. Guards the
10747 // paired-site convergence just applied at caixa-flux's
10748 // [`ClusterBundleOpts::for_caixa`] `git_url` composer (which
10749 // now routes through this accessor): a future implementation of
10750 // this method that reordered the format arguments, swapped the
10751 // `<org>` constant for a different one, or interposed a
10752 // canonicalization pass on the `Some` arm surfaces here as a
10753 // caixa-core build-time test failure rather than as a downstream
10754 // FluxCD `GitRepository` reconcile mismatch far from this
10755 // method's source.
10756 for repositorio in [
10757 None,
10758 Some("github:pleme-io/hello-rio"),
10759 Some("https://github.com/pleme-io/checkout"),
10760 Some("ssh://git@github.com/pleme-io/checkout.git"),
10761 ] {
10762 let c = caixa_with_repositorio(repositorio);
10763 let manual = c.repositorio().map_or_else(
10764 || {
10765 format!(
10766 "https://github.com/{org}/{nome}",
10767 org = crate::DEFAULT_PLEME_GIT_ORG,
10768 nome = c.nome(),
10769 )
10770 },
10771 str::to_owned,
10772 );
10773 assert_eq!(
10774 c.canonical_git_url(),
10775 manual,
10776 "Caixa::canonical_git_url must byte-equal the manual \
10777 open-coded `repositorio().map(str::to_owned)\
10778 .unwrap_or_else(|| format!(...))` composition across \
10779 every representative :repositorio input — got {:?}, \
10780 expected {manual:?}",
10781 c.canonical_git_url(),
10782 );
10783 }
10784 }
10785
10786 // ── Caixa::publish_tag — resolved-publish-tag composer ───────────
10787
10788 #[test]
10789 fn publish_tag_composes_prefix_and_versao_on_all_shapes() {
10790 // Fail-before-pass-after pin: [`Caixa::publish_tag`] must compose
10791 // [`crate::DEFAULT_PUBLISH_TAG_PREFIX`] against the caixa's typed
10792 // [`Caixa::versao`] byte-string across every SemVer-2 shape the
10793 // sibling [`validate_versao_accepts_canonical_forms`] positive-set
10794 // sweep documents — bare MAJOR.MINOR.PATCH, pre-release tags
10795 // (`-rc.1`), build metadata (`+build.42`), the combined form, and
10796 // the `0.0.0` boundary case. Every accept-set value the peer
10797 // validate gate lets through must survive the resolved-tag
10798 // projection byte-equal.
10799 for versao in [
10800 "0.1.0",
10801 "0.0.0",
10802 "1.0.0",
10803 "1.2.3-rc.1",
10804 "1.2.3+build.42",
10805 "1.2.3-rc.1+build.42",
10806 ] {
10807 let c = caixa_with_versao(versao);
10808 let expected = format!(
10809 "{prefix}{versao}",
10810 prefix = crate::DEFAULT_PUBLISH_TAG_PREFIX,
10811 );
10812 assert_eq!(
10813 c.publish_tag(),
10814 expected,
10815 "Caixa::publish_tag must compose \
10816 DEFAULT_PUBLISH_TAG_PREFIX ({prefix:?}) against \
10817 :versao ({versao:?}) verbatim — got {got:?}, \
10818 expected {expected:?}",
10819 prefix = crate::DEFAULT_PUBLISH_TAG_PREFIX,
10820 got = c.publish_tag(),
10821 );
10822 }
10823 }
10824
10825 #[test]
10826 fn publish_tag_starts_with_default_publish_tag_prefix() {
10827 // Prefix-shape pin: every [`Caixa::publish_tag`] emission must
10828 // begin with the canonical [`crate::DEFAULT_PUBLISH_TAG_PREFIX`]
10829 // byte-string on every input, guarding a hypothetical future
10830 // implementation that migrated the prefix segment to an inline
10831 // literal (`"v"`) that would silently drift from any rebrand of
10832 // the lifted constant. Peer to the sibling caixa-flux
10833 // `cluster_bundle_default_git_tag_uses_lifted_caixa_core_prefix`
10834 // test which pins the same prefix invariant at the reader-side
10835 // `GitRefSpec::Tag` emit site.
10836 for versao in ["0.0.0", "0.1.0", "1.2.3-rc.1", "9.9.9+build.1"] {
10837 let c = caixa_with_versao(versao);
10838 let tag = c.publish_tag();
10839 assert!(
10840 tag.starts_with(crate::DEFAULT_PUBLISH_TAG_PREFIX),
10841 "Caixa::publish_tag emission {tag:?} must start with \
10842 the lifted crate::DEFAULT_PUBLISH_TAG_PREFIX \
10843 ({prefix:?})",
10844 prefix = crate::DEFAULT_PUBLISH_TAG_PREFIX,
10845 );
10846 }
10847 }
10848
10849 #[test]
10850 fn publish_tag_byte_matches_manual_composition() {
10851 // Byte-parity pin: [`Caixa::publish_tag`] must render byte-
10852 // identically to the manual open-coded
10853 // `format!("{prefix}{versao}", prefix =
10854 // caixa_core::DEFAULT_PUBLISH_TAG_PREFIX, versao =
10855 // caixa.versao())` composition every prior substrate-side
10856 // caller re-derived. Guards the paired-site convergence just
10857 // applied at caixa-flux's [`ClusterBundleOpts::for_caixa`]
10858 // `git_ref` composer (which now routes through this accessor):
10859 // a future implementation of this method that reordered the
10860 // format arguments, swapped the `<prefix>` constant for a
10861 // different one, or interposed a canonicalization pass on the
10862 // `:versao` axis surfaces here as a caixa-core build-time test
10863 // failure rather than as a downstream FluxCD `GitRepository`
10864 // reconcile mismatch far from this method's source.
10865 for versao in [
10866 "0.1.0",
10867 "0.0.0",
10868 "1.2.3-rc.1",
10869 "1.2.3+build.42",
10870 "1.2.3-rc.1+build.42",
10871 ] {
10872 let c = caixa_with_versao(versao);
10873 let manual = format!(
10874 "{prefix}{versao}",
10875 prefix = crate::DEFAULT_PUBLISH_TAG_PREFIX,
10876 versao = c.versao(),
10877 );
10878 assert_eq!(
10879 c.publish_tag(),
10880 manual,
10881 "Caixa::publish_tag must byte-equal the manual \
10882 open-coded `format!(\"{{prefix}}{{versao}}\", ...)` \
10883 composition across every representative :versao input \
10884 — got {got:?}, expected {manual:?}",
10885 got = c.publish_tag(),
10886 );
10887 }
10888 }
10889
10890 // ── Caixa::lareira_chart_name — resolved-chart-name composer ─────
10891
10892 #[test]
10893 fn lareira_chart_name_composes_prefix_and_nome_on_all_shapes() {
10894 // Fail-before-pass-after pin: [`Caixa::lareira_chart_name`] must
10895 // compose [`crate::LAREIRA_CHART_NAME_PREFIX`] against the caixa's
10896 // typed [`Caixa::nome`] byte-string across every DNS-1123 shape
10897 // the sibling [`validate_nome_accepts_canonical_forms`] positive-
10898 // set sweep documents — single-word, hyphen-joined, version-
10899 // suffixed, single-char, two-char, digit-start, retry-suffixed.
10900 // Every accept-set value the peer validate gate lets through must
10901 // survive the resolved-chart-name projection byte-equal.
10902 for nome in [
10903 "checkout",
10904 "cart-v2",
10905 "a",
10906 "db",
10907 "3rd-party-shim",
10908 "payment-retry",
10909 "0",
10910 ] {
10911 let c = caixa_with_nome(nome);
10912 let expected = format!("{prefix}{nome}", prefix = crate::LAREIRA_CHART_NAME_PREFIX);
10913 assert_eq!(
10914 c.lareira_chart_name(),
10915 expected,
10916 "Caixa::lareira_chart_name must compose \
10917 LAREIRA_CHART_NAME_PREFIX ({prefix:?}) against \
10918 :nome ({nome:?}) verbatim — got {got:?}, \
10919 expected {expected:?}",
10920 prefix = crate::LAREIRA_CHART_NAME_PREFIX,
10921 got = c.lareira_chart_name(),
10922 );
10923 }
10924 }
10925
10926 #[test]
10927 fn lareira_chart_name_starts_with_lifted_prefix() {
10928 // Prefix-shape pin: every [`Caixa::lareira_chart_name`] emission
10929 // must begin with the canonical
10930 // [`crate::LAREIRA_CHART_NAME_PREFIX`] byte-string on every
10931 // input, guarding a hypothetical future implementation that
10932 // migrated the prefix segment to an inline literal (`"lareira-"`)
10933 // that would silently drift from any rebrand of the lifted
10934 // constant. Peer to the sibling
10935 // [`publish_tag_starts_with_default_publish_tag_prefix`] pin on
10936 // the co-resident resolved-publish-tag composer's prefix axis.
10937 for nome in ["checkout", "cart", "a", "payment-retry", "0"] {
10938 let c = caixa_with_nome(nome);
10939 let chart = c.lareira_chart_name();
10940 assert!(
10941 chart.starts_with(crate::LAREIRA_CHART_NAME_PREFIX),
10942 "Caixa::lareira_chart_name emission {chart:?} must start \
10943 with the lifted crate::LAREIRA_CHART_NAME_PREFIX \
10944 ({prefix:?})",
10945 prefix = crate::LAREIRA_CHART_NAME_PREFIX,
10946 );
10947 }
10948 }
10949
10950 #[test]
10951 fn lareira_chart_name_byte_matches_canonical_helper_composition() {
10952 // Byte-parity pin: [`Caixa::lareira_chart_name`] must render
10953 // byte-identically to the manual open-coded
10954 // `caixa_core::lareira_chart_name(caixa.nome())` two-step
10955 // composition every prior substrate-side caller re-derived.
10956 // Guards the paired-site convergence just applied at caixa-helm's
10957 // [`render_chart_for_servico_with`] `ChartDir.name` composer,
10958 // caixa-flux's [`cluster_bundle`] per-CR `chart_name` binding,
10959 // and caixa-tatara's [`process_for_aplicacao`] `release_name`
10960 // composer (all of which now route through this accessor): a
10961 // future implementation of this method that reordered the
10962 // composition arguments, swapped the `<prefix>` constant for a
10963 // different one, or interposed a canonicalization pass on the
10964 // `:nome` axis surfaces here as a caixa-core build-time test
10965 // failure rather than as a downstream Helm chart-render / FluxCD
10966 // reconcile / tatara Process-CR mismatch far from this method's
10967 // source.
10968 for nome in [
10969 "checkout",
10970 "cart-v2",
10971 "a",
10972 "db",
10973 "3rd-party-shim",
10974 "payment-retry",
10975 ] {
10976 let c = caixa_with_nome(nome);
10977 let manual = crate::lareira_chart_name(c.nome());
10978 assert_eq!(
10979 c.lareira_chart_name(),
10980 manual,
10981 "Caixa::lareira_chart_name must byte-equal the manual \
10982 open-coded `caixa_core::lareira_chart_name(caixa.nome())` \
10983 composition across every representative :nome input — \
10984 got {got:?}, expected {manual:?}",
10985 got = c.lareira_chart_name(),
10986 );
10987 }
10988 }
10989
10990 // ── Caixa::oci_chart_ref — resolved-OCI-chart-ref composer ────────
10991
10992 #[test]
10993 fn oci_chart_ref_composes_scheme_and_lareira_chart_name_on_all_shapes() {
10994 // Fail-before-pass-after pin: [`Caixa::oci_chart_ref`] must
10995 // compose [`crate::OCI_SCHEME_PREFIX`] + the caller-supplied
10996 // `registry` + [`crate::lareira_chart_name`]-of-[`Caixa::nome`]
10997 // across the full paired `(registry, :nome)` accept-set — every
10998 // representative registry the substrate-side emitters carry
10999 // (`ghcr.io/pleme-io/charts`, the canonical CAIXA-SDLC §II
11000 // ArtifactHub-tier registry; `ghcr.io/pleme-io`, the bare-org
11001 // arm the sibling `oci_chart_ref_pins_byte_shape_against_prior_
11002 // inline_format` render-side pin exercises; `registry.example.
11003 // com`, an off-org shape; `localhost:5000`, the local-dev shape
11004 // every `feira chart` iteration path lands under) × every DNS-
11005 // 1123 `:nome` shape the peer `validate_nome_accepts_canonical_
11006 // forms` positive-set sweep documents (single-word, hyphen-
11007 // joined, single-char, two-char, digit-start, retry-suffixed).
11008 // Every accept-set pair the peer validate gates let through must
11009 // survive the resolved-OCI-ref projection byte-equal.
11010 for registry in [
11011 "ghcr.io/pleme-io/charts",
11012 "ghcr.io/pleme-io",
11013 "registry.example.com",
11014 "localhost:5000",
11015 ] {
11016 for nome in [
11017 "checkout",
11018 "cart-v2",
11019 "a",
11020 "db",
11021 "3rd-party-shim",
11022 "payment-retry",
11023 "0",
11024 ] {
11025 let c = caixa_with_nome(nome);
11026 let expected = format!(
11027 "{scheme}{registry}/{chart}",
11028 scheme = crate::OCI_SCHEME_PREFIX,
11029 chart = crate::lareira_chart_name(nome),
11030 );
11031 assert_eq!(
11032 c.oci_chart_ref(registry),
11033 expected,
11034 "Caixa::oci_chart_ref must compose \
11035 OCI_SCHEME_PREFIX ({scheme:?}) + registry ({registry:?}) + \
11036 lareira_chart_name(:nome ({nome:?})) verbatim — got {got:?}, \
11037 expected {expected:?}",
11038 scheme = crate::OCI_SCHEME_PREFIX,
11039 got = c.oci_chart_ref(registry),
11040 );
11041 }
11042 }
11043 }
11044
11045 #[test]
11046 fn oci_chart_ref_starts_with_lifted_scheme_prefix() {
11047 // Scheme-prefix-shape pin: every [`Caixa::oci_chart_ref`]
11048 // emission must begin with the canonical
11049 // [`crate::OCI_SCHEME_PREFIX`] byte-string on every input, guarding
11050 // a hypothetical future implementation that migrated the scheme
11051 // segment to an inline literal (`"oci://"`) that would silently
11052 // drift from any rebrand of the lifted constant. Peer to the
11053 // sibling [`publish_tag_starts_with_default_publish_tag_prefix`]
11054 // + [`lareira_chart_name_starts_with_lifted_prefix`] pins on the
11055 // co-resident resolved-publish-tag / resolved-chart-name
11056 // composers' prefix axes.
11057 for registry in [
11058 "ghcr.io/pleme-io/charts",
11059 "ghcr.io/pleme-io",
11060 "localhost:5000",
11061 ] {
11062 for nome in ["checkout", "cart", "a", "payment-retry", "0"] {
11063 let c = caixa_with_nome(nome);
11064 let ref_ = c.oci_chart_ref(registry);
11065 assert!(
11066 ref_.starts_with(crate::OCI_SCHEME_PREFIX),
11067 "Caixa::oci_chart_ref emission {ref_:?} must start \
11068 with the lifted crate::OCI_SCHEME_PREFIX ({scheme:?}) \
11069 — registry ({registry:?}), :nome ({nome:?})",
11070 scheme = crate::OCI_SCHEME_PREFIX,
11071 );
11072 }
11073 }
11074 }
11075
11076 #[test]
11077 fn oci_chart_ref_byte_matches_canonical_helper_composition() {
11078 // Byte-parity pin: [`Caixa::oci_chart_ref`] must render byte-
11079 // identically to the manual open-coded
11080 // `caixa_core::oci_chart_ref(registry, caixa.nome())` two-step
11081 // composition every prior substrate-side caller re-derived.
11082 // Guards the paired-site convergence just applied at caixa-
11083 // tatara's [`derive_chart_ref`] helper (which now routes through
11084 // this accessor): a future implementation of this method that
11085 // reordered the composition arguments, swapped the `<scheme>`
11086 // constant for a different one, migrated the `<chart>` segment
11087 // off the paired [`crate::lareira_chart_name`] composer, or
11088 // interposed a canonicalization pass on either input axis
11089 // surfaces here as a caixa-core build-time test failure rather
11090 // than as a downstream `helm install` / FluxCD OCI-source
11091 // reconcile / tatara `Process`-CR mismatch far from this
11092 // method's source. Sibling to the peer
11093 // [`lareira_chart_name_byte_matches_canonical_helper_composition`]
11094 // / [`publish_tag_byte_matches_manual_composition`] /
11095 // [`canonical_git_url_byte_matches_manual_composition`] byte-
11096 // parity pins that carry the same discipline on the co-resident
11097 // resolved-chart-name / resolved-publish-tag / resolved-git-URL
11098 // composers.
11099 for registry in [
11100 "ghcr.io/pleme-io/charts",
11101 "ghcr.io/pleme-io",
11102 "registry.example.com",
11103 "localhost:5000",
11104 ] {
11105 for nome in [
11106 "checkout",
11107 "cart-v2",
11108 "a",
11109 "db",
11110 "3rd-party-shim",
11111 "payment-retry",
11112 ] {
11113 let c = caixa_with_nome(nome);
11114 let manual = crate::oci_chart_ref(registry, c.nome());
11115 assert_eq!(
11116 c.oci_chart_ref(registry),
11117 manual,
11118 "Caixa::oci_chart_ref must byte-equal the manual \
11119 open-coded `caixa_core::oci_chart_ref(registry, \
11120 caixa.nome())` composition across every representative \
11121 (registry, :nome) pair — registry ({registry:?}), \
11122 :nome ({nome:?}), got {got:?}, expected {manual:?}",
11123 got = c.oci_chart_ref(registry),
11124 );
11125 }
11126 }
11127 }
11128
11129 // ── Caixa::descricao — outer top-level Option<&str> scalar accessor ──
11130
11131 #[test]
11132 fn descricao_returns_descricao_byte_string_verbatim_across_permutations() {
11133 // The canonical per-`Caixa` `:descricao` free-form-prose scalar
11134 // pin: [`Caixa::descricao`] must return the `:descricao` typed
11135 // byte-string verbatim as an `Option<&str>`, byte-equal to the
11136 // raw `self.descricao.as_deref()` access across every
11137 // representative value in the accept-set — `None` (the "omit
11138 // the slot to defer to the per-renderer `caixa.nome`-derived
11139 // fallback" arm every existing fixture without a `:descricao`
11140 // line carries), `Some("")` (a past-the-guard sentinel that
11141 // pins the accessor doesn't perform a silent `Some("") → None`
11142 // collapse on the empty arm — validate rejects `Some("")`
11143 // through `DescricaoEmpty` but the accessor must ship the raw
11144 // slot verbatim so a validate-time gate regression surfaces at
11145 // the caixa-helm / caixa-feira emit boundary rather than being
11146 // silently absorbed into the per-renderer `caixa.nome`-derived
11147 // fallback), `Some("Checkout flow.")` (the canonical one-line
11148 // prose descriptor the peer
11149 // `validate_descricao_accepts_canonical_value` positive sweep
11150 // exercises), `Some("Canonical Rust→wasm32-wasip2 caixa
11151 // Servico.")` (the multi-byte Unicode continuation-byte shape
11152 // the `hello-rio` fixture carries), `Some("→ — · ✓")` (a
11153 // multi-glyph Unicode shape the peer
11154 // `is_chart_description_shape` predicate accepts), and five
11155 // past-the-guard sentinels for the `DescricaoInvalid` refusal
11156 // cases (`Some(" Checkout flow.")` leading-whitespace,
11157 // `Some("Checkout flow. ")` trailing-whitespace,
11158 // `Some("Checkout\nflow.")` embedded-LF,
11159 // `Some("Checkout\tflow.")` embedded-TAB, and
11160 // `Some("Checkout\x00flow.")` embedded-NUL — the sentinels pin
11161 // the accessor doesn't silently absorb the refusal cases into
11162 // a fallback).
11163 //
11164 // Third outer top-level [`Caixa`] `Option<&str>`-return scalar
11165 // accessor pin on the substrate primitive — sibling of the peer
11166 // [`Caixa::licenca`] (6d5bc28) and [`Caixa::repositorio`]
11167 // (cc7332d) pins that opened the "outer [`Caixa`]
11168 // `Option<&str>` scalar" projection pin pattern this pin folds
11169 // on. Sibling in shape to the peer per-`:placement`
11170 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
11171 // [`crate::aplicacao::Placement::affinity`] (74ec2d3) accessor
11172 // pins on the sibling per-M3-mesh-slot `Option<&str>`-return
11173 // axes, extended onto the outer top-level [`Caixa`] universal-
11174 // axis surface. Pins against a future silent detour that
11175 // returned an owned `Option<String>` (which would type-check
11176 // but silently allocate on every accessor call, breaking the
11177 // zero-cost projection every peer sibling accessor carries), a
11178 // `Some("") → None` collapse (which would silently absorb the
11179 // `DescricaoEmpty` refusal case at the accessor boundary and
11180 // the caixa-helm `Chart.yaml` `description:` fold would
11181 // silently render a `caixa.nome`-derived fallback on a
11182 // struct-literal `Caixa { descricao: Some(""), .. }`), or a
11183 // `None → Some(<default>)` collapse (which would silently
11184 // reify the per-renderer `caixa.nome`-derived fallback at the
11185 // accessor boundary and every downstream consumer keying off
11186 // the `Option::is_none()` discriminator would lose the "author
11187 // omitted the slot" signal).
11188 for descricao in [
11189 None,
11190 Some(""),
11191 Some("Checkout flow."),
11192 Some("Canonical Rust→wasm32-wasip2 caixa Servico."),
11193 Some("→ — · ✓"),
11194 Some(" Checkout flow."),
11195 Some("Checkout flow. "),
11196 Some("Checkout\nflow."),
11197 Some("Checkout\tflow."),
11198 Some("Checkout\x00flow."),
11199 ] {
11200 let c = caixa_with_descricao(descricao);
11201 assert_eq!(
11202 c.descricao(),
11203 descricao,
11204 "Caixa::descricao must return :descricao verbatim (got \
11205 {:?}, expected {descricao:?})",
11206 c.descricao(),
11207 );
11208 assert_eq!(
11209 c.descricao(),
11210 c.descricao.as_deref(),
11211 "Caixa::descricao must byte-equal the raw \
11212 `self.descricao.as_deref()` field access across every \
11213 value in the Option<&str> accept-set",
11214 );
11215 }
11216 }
11217
11218 #[test]
11219 fn validate_descricao_empty_arm_routes_through_accessor() {
11220 // Composition pin: [`Caixa::validate_descricao`]'s empty-arm
11221 // gate must key off [`Caixa::descricao`], not the raw
11222 // `self.descricao.as_deref()` field access. Structurally: a
11223 // `Caixa { descricao: Some(""), .. }` must surface the
11224 // `DescricaoEmpty` refusal exactly, and a
11225 // `Caixa { descricao: Some("Checkout flow."), .. }` (the
11226 // canonical one-line-prose form) must pass validate. The pair
11227 // jointly pins the accessor + validate-gate composition: any
11228 // future silent detour that had the accessor return `None` on
11229 // the empty arm (a `.filter(|s| !s.is_empty())` collapse) would
11230 // silently absorb the `DescricaoEmpty` refusal at the accessor
11231 // boundary and the validate gate would accept a struct-literal
11232 // `Caixa { descricao: Some(""), .. }` — the composition pin
11233 // catches that at caixa-core build time.
11234 //
11235 // Peer of the [`Caixa::licenca`] (6d5bc28)
11236 // `validate_licenca_empty_arm_routes_through_accessor` and
11237 // [`Caixa::repositorio`] (cc7332d)
11238 // `validate_repositorio_empty_arm_routes_through_accessor`
11239 // composition pins on the sibling outer top-level [`Caixa`]
11240 // `Option<&str>` universal-axis surface — same "the validate /
11241 // shape-gate predicate must route through the substrate-
11242 // primitive typed dispatch" discipline extended onto the third
11243 // outer top-level [`Caixa`] universal-axis `Option<&str>`-
11244 // composition surface.
11245 let c = caixa_with_descricao(Some(""));
11246 assert!(
11247 matches!(c.validate_descricao(), Err(ManifestError::DescricaoEmpty),),
11248 "validate_descricao must reject descricao == Some(\"\") \
11249 with DescricaoEmpty — the accessor and the validate gate \
11250 must route through the same substrate-primitive typed \
11251 dispatch on the :descricao empty arm",
11252 );
11253 let c = caixa_with_descricao(Some("Checkout flow."));
11254 assert!(
11255 c.validate_descricao().is_ok(),
11256 "validate_descricao must accept descricao == \
11257 Some(\"Checkout flow.\") (the canonical one-line-prose \
11258 chart-description shape)",
11259 );
11260 }
11261
11262 #[test]
11263 fn descricao_projects_option_str_by_borrow() {
11264 // The by-borrow pin: [`Caixa::descricao`] returns
11265 // `Option<&str>` by borrow — the `&str` borrows the underlying
11266 // `String` storage of the `Option<String>` slot and the
11267 // accessor must not allocate a fresh `String` on every call.
11268 // Peer of the [`Caixa::licenca`] (6d5bc28) and
11269 // [`Caixa::repositorio`] (cc7332d) by-borrow pins on the peer
11270 // outer top-level [`Caixa`] `Option<&str>`-return axes, and of
11271 // the per-`:placement`
11272 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) by-
11273 // borrow pin on the peer per-M3-mesh-slot `Option<&str>`-
11274 // return axis, extended onto the third outer top-level
11275 // [`Caixa`] universal-axis `Option<&str>` shape — the
11276 // accessor's returned `&str` must borrow from `&self` (the
11277 // returned reference's lifetime is tied to `&self`), and
11278 // calling the accessor twice on the same [`Caixa`] must yield
11279 // the same `Option<&str>` verbatim (idempotent, no side
11280 // effects on `&self`).
11281 //
11282 // Pins against a future silent detour that returned an owned
11283 // `Option<String>` (which would type-check but silently
11284 // allocate on every call, breaking the zero-cost projection
11285 // every peer sibling accessor carries), or a one-arm-only
11286 // accessor that returned a saturating value on some sentinel
11287 // input (breaking the pass-through invariant the sibling
11288 // required-scalar accessors carry).
11289 for descricao in [
11290 None,
11291 Some(""),
11292 Some("Checkout flow."),
11293 Some("Canonical Rust→wasm32-wasip2 caixa Servico."),
11294 ] {
11295 let c = caixa_with_descricao(descricao);
11296 let first = c.descricao();
11297 let second = c.descricao();
11298 assert_eq!(
11299 first, second,
11300 "Caixa::descricao must be idempotent — two successive \
11301 calls on the same &self must return the same \
11302 Option<&str>",
11303 );
11304 assert_eq!(
11305 first, descricao,
11306 "Caixa::descricao must return :descricao verbatim by \
11307 borrow — got {first:?}, expected {descricao:?}",
11308 );
11309 }
11310 }
11311
11312 // ── validate_edicao — universal-axis language-edition shape ──
11313
11314 fn caixa_with_edicao(edicao: Option<&str>) -> Caixa {
11315 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
11316 c.edicao = edicao.map(String::from);
11317 c
11318 }
11319
11320 #[test]
11321 fn validate_edicao_accepts_none() {
11322 // The omit-the-slot identity: `:edicao` is optional. The
11323 // gate is a no-op when the author didn't declare a value —
11324 // every caixa without an `:edicao` line trivially passes,
11325 // and the substrate-side build pipeline falls back to the
11326 // documented default edition. Mirrors the peer
11327 // `validate_licenca_accepts_none` posture on the sibling
11328 // `Option<String>` Caixa slot.
11329 let c = caixa_with_edicao(None);
11330 c.validate_edicao().unwrap();
11331 }
11332
11333 #[test]
11334 fn validate_edicao_accepts_canonical_value() {
11335 // Positive control: the canonical `"2026"` edition every
11336 // existing renderer-side fixture (`caixa-helm`, `caixa-flux`,
11337 // `caixa-mesh`) carries by construction passes the gate.
11338 // Future-introduced sibling editions (`"2027"`, `"2030"`,
11339 // `"2049"`) that match the same 4-digit ASCII decimal year
11340 // shape must also trivially pass — the structural shape
11341 // predicate accepts every well-formed year regardless of
11342 // whether the substrate yet understands the specific value
11343 // (a future known-edition allowlist tightens that).
11344 for ed in ["2026", "2027", "2030", "2049"] {
11345 let c = caixa_with_edicao(Some(ed));
11346 c.validate_edicao()
11347 .unwrap_or_else(|err| panic!("canonical {ed:?} must pass: {err:?}"));
11348 }
11349 }
11350
11351 #[test]
11352 fn validate_edicao_rejects_empty_some() {
11353 // Canonical paste-from-blank-doc footgun. Without this gate
11354 // the empty `Some("")` silently lands as `(:edicao "")` in
11355 // the rendered caixa.lisp and a future renderer-side
11356 // consumer's `Option::unwrap_or_else` (which only fires on
11357 // `None`) skips its fallback. Mirrors the peer
11358 // [`ManifestError::LicencaEmpty`] empty-arm on the sibling
11359 // `Option<String>` Caixa slot.
11360 let c = caixa_with_edicao(Some(""));
11361 let err = c.validate_edicao().unwrap_err();
11362 assert!(matches!(err, ManifestError::EdicaoEmpty), "got {err:?}",);
11363 }
11364
11365 #[test]
11366 fn validate_edicao_rejects_free_form_non_year() {
11367 // Free-form non-year footgun: the bare `"x"` / `"latest"` /
11368 // `"nightly"` shapes carry no operational meaning on the
11369 // substrate's build-time edition selector. Until this gate
11370 // landed the bare empty-arm check let every such value
11371 // through and broke far from the source caixa.lisp. Peer
11372 // with the shape-predicate cascade
11373 // `validate_repositorio_rejects_missing_colon_separator`
11374 // establishes past its own empty arm.
11375 for ed in ["x", "latest", "nightly", "stable"] {
11376 let c = caixa_with_edicao(Some(ed));
11377 let err = c.validate_edicao().unwrap_err();
11378 assert!(
11379 matches!(err, ManifestError::EdicaoInvalid { .. }),
11380 "expected EdicaoInvalid on {ed:?}, got {err:?}",
11381 );
11382 }
11383 }
11384
11385 #[test]
11386 fn validate_edicao_rejects_trailing_whitespace() {
11387 // Paste-from-doc whitespace footgun. A trailing space in
11388 // the `:edicao` value would silently break the substrate's
11389 // build-time edition match-table lookup at the rendered
11390 // artifact's edition-selector consumer. The shape predicate
11391 // refuses every whitespace byte by construction (any byte
11392 // outside `0-9` fails `is_ascii_digit`). Peer with
11393 // `validate_repositorio_rejects_whitespace`.
11394 let c = caixa_with_edicao(Some("2026 "));
11395 let err = c.validate_edicao().unwrap_err();
11396 let ManifestError::EdicaoInvalid { edicao, .. } = err else {
11397 panic!("expected EdicaoInvalid, got {err:?}");
11398 };
11399 assert_eq!(edicao, "2026 ");
11400 }
11401
11402 #[test]
11403 fn validate_edicao_rejects_leading_whitespace() {
11404 // Symmetric paste-from-doc whitespace footgun on the leading
11405 // boundary — the gate refuses every shape with a non-digit
11406 // byte by construction.
11407 let c = caixa_with_edicao(Some(" 2026"));
11408 let err = c.validate_edicao().unwrap_err();
11409 assert!(
11410 matches!(err, ManifestError::EdicaoInvalid { .. }),
11411 "got {err:?}",
11412 );
11413 }
11414
11415 #[test]
11416 fn validate_edicao_rejects_control_char() {
11417 // Paste-from-multiline-doc CRLF footgun — control characters
11418 // at the value boundary break the substrate's build-time
11419 // edition-selector parser. Peer with
11420 // `validate_repositorio_rejects_control_char`.
11421 let c = caixa_with_edicao(Some("2026\n"));
11422 let err = c.validate_edicao().unwrap_err();
11423 assert!(
11424 matches!(err, ManifestError::EdicaoInvalid { .. }),
11425 "got {err:?}",
11426 );
11427 }
11428
11429 #[test]
11430 fn validate_edicao_rejects_non_ascii_lookalike() {
11431 // Fullwidth-keyboard look-alike footgun — `"2026"` is
11432 // the U+FF12 U+FF10 U+FF12 U+FF16 sequence (CJK fullwidth
11433 // digits), 4 codepoints but 12 UTF-8 bytes; the substrate's
11434 // edition selector wants an ASCII year, and the gate
11435 // refuses every non-ASCII shape by construction (length in
11436 // bytes is 12 ≠ 4, *and* every byte falls outside
11437 // `is_ascii_digit`'s `0-9` range).
11438 let c = caixa_with_edicao(Some("2026"));
11439 let err = c.validate_edicao().unwrap_err();
11440 assert!(
11441 matches!(err, ManifestError::EdicaoInvalid { .. }),
11442 "got {err:?}",
11443 );
11444 }
11445
11446 #[test]
11447 fn validate_edicao_rejects_version_tag_prefix() {
11448 // Common version-tag idiom footgun — `"v2026"` / `"e2026"`
11449 // / `"r2026"` are familiar shapes from git-tag / Rust
11450 // edition / release-tag conventions that don't apply to
11451 // the year-shaped edition axis. The shape predicate refuses
11452 // every leading non-digit prefix.
11453 for ed in ["v2026", "e2026", "r2026"] {
11454 let c = caixa_with_edicao(Some(ed));
11455 let err = c.validate_edicao().unwrap_err();
11456 assert!(
11457 matches!(err, ManifestError::EdicaoInvalid { .. }),
11458 "expected EdicaoInvalid on {ed:?}, got {err:?}",
11459 );
11460 }
11461 }
11462
11463 #[test]
11464 fn validate_edicao_rejects_decimal_shape() {
11465 // Decimal-shaped pseudo-version footgun — `"2026.1"` /
11466 // `"2026.0"` are familiar shapes from semver / float
11467 // conventions that don't apply to the year-shaped edition
11468 // axis. The shape predicate refuses every non-digit byte
11469 // (`.` falls outside `is_ascii_digit`).
11470 for ed in ["2026.1", "2026.0", "2026.0.1"] {
11471 let c = caixa_with_edicao(Some(ed));
11472 let err = c.validate_edicao().unwrap_err();
11473 assert!(
11474 matches!(err, ManifestError::EdicaoInvalid { .. }),
11475 "expected EdicaoInvalid on {ed:?}, got {err:?}",
11476 );
11477 }
11478 }
11479
11480 #[test]
11481 fn validate_edicao_rejects_wrong_length_numeric() {
11482 // Wrong-length numeric footgun — `"26"` (truncated) /
11483 // `"202"` (truncated) / `"20260"` (extra digit) / `"00026"`
11484 // (zero-padded too wide) all parse as integers but don't
11485 // name a 4-digit year. The shape predicate refuses every
11486 // value whose length isn't exactly 4 bytes.
11487 for ed in ["26", "202", "20260", "00026", "9"] {
11488 let c = caixa_with_edicao(Some(ed));
11489 let err = c.validate_edicao().unwrap_err();
11490 assert!(
11491 matches!(err, ManifestError::EdicaoInvalid { .. }),
11492 "expected EdicaoInvalid on {ed:?}, got {err:?}",
11493 );
11494 }
11495 }
11496
11497 #[test]
11498 fn validate_edicao_empty_takes_precedence_over_shape() {
11499 // Empty-first cascade pin: the empty `Some("")` surfaces
11500 // the narrower `EdicaoEmpty` not the shape-predicate-
11501 // wrapped `EdicaoInvalid`, mirroring the peer
11502 // `validate_repositorio_empty_takes_precedence_over_shape`
11503 // (`RepositorioEmpty` → `RepositorioInvalid`),
11504 // `NomeEmpty` → `NomeInvalid`, `VersaoEmpty` →
11505 // `VersaoInvalid`, `FonteRepoEmpty` → `FonteRepoInvalid`
11506 // cascades. The shape predicate also refuses the empty
11507 // input (defensively — `s.len() != 4`), but the
11508 // manifest-layer empty arm runs first to surface the
11509 // narrower diagnostic verbatim.
11510 let c = caixa_with_edicao(Some(""));
11511 let err = c.validate_edicao().unwrap_err();
11512 assert!(matches!(err, ManifestError::EdicaoEmpty), "got {err:?}",);
11513 }
11514
11515 #[test]
11516 fn validate_edicao_template_passes() {
11517 // Round-trip pin: the bare `Caixa::template` shape (which
11518 // carries `:edicao "2026"` verbatim) passes the gate by
11519 // construction. A future template-shape change that
11520 // introduced `(:edicao "")` or a non-year value would
11521 // surface here as a regression. Mirrors the peer
11522 // `validate_licenca_template_passes` pin.
11523 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
11524 c.validate_edicao().unwrap();
11525 }
11526
11527 #[test]
11528 fn validate_edicao_diagnostic_names_offending_slot() {
11529 // Diagnostic-shape pin (peer with
11530 // `validate_licenca_diagnostic_names_offending_slot`): the
11531 // error's Display surfaces the `:edicao` slot name verbatim,
11532 // so a `feira lint` run can render the diagnostic without
11533 // re-parsing and the author can grep their caixa.lisp for
11534 // the offending `:edicao` line.
11535 let c = caixa_with_edicao(Some(""));
11536 let rendered = c.validate_edicao().unwrap_err().to_string();
11537 assert!(
11538 rendered.contains(":edicao"),
11539 "diagnostic must name the offending slot: {rendered}",
11540 );
11541 }
11542
11543 #[test]
11544 fn validate_edicao_invalid_diagnostic_carries_offending_value() {
11545 // Diagnostic-shape pin on the shape-predicate arm (peer
11546 // with `validate_repositorio_diagnostic_carries_offending_value`):
11547 // the error's Display surfaces the offending value + slot
11548 // name verbatim, so a `feira lint` run can render the
11549 // diagnostic without re-parsing and the author can grep
11550 // their caixa.lisp for the offending `:edicao` value.
11551 let c = caixa_with_edicao(Some("v2026"));
11552 let rendered = c.validate_edicao().unwrap_err().to_string();
11553 assert!(
11554 rendered.contains(":edicao"),
11555 "diagnostic must name the offending slot: {rendered}",
11556 );
11557 assert!(
11558 rendered.contains("v2026"),
11559 "diagnostic must quote the offending value: {rendered}",
11560 );
11561 }
11562
11563 // ── Caixa::edicao — outer top-level Option<&str> scalar accessor ──
11564
11565 #[test]
11566 fn edicao_returns_edicao_byte_string_verbatim_across_permutations() {
11567 // The canonical per-`Caixa` `:edicao` language-edition scalar
11568 // pin: [`Caixa::edicao`] must return the `:edicao` typed
11569 // byte-string verbatim as an `Option<&str>`, byte-equal to the
11570 // raw `self.edicao.as_deref()` access across every representative
11571 // value in the accept-set — `None` (the "omit the slot to defer
11572 // to the substrate's default edition" arm every existing
11573 // [`caixa-resolver`] fixture without an `:edicao` line carries),
11574 // `Some("")` (a past-the-guard sentinel that pins the accessor
11575 // doesn't perform a silent `Some("") → None` collapse on the
11576 // empty arm — validate rejects `Some("")` through `EdicaoEmpty`
11577 // but the accessor must ship the raw slot verbatim so a
11578 // validate-time gate regression surfaces at any future edition-
11579 // aware consumer's boundary rather than being silently absorbed
11580 // into the substrate's default edition), `Some("2026")` (the
11581 // canonical 4-digit-ASCII-decimal-year shape every `feira init`
11582 // template scaffolds via [`Caixa::template`] and every
11583 // renderer-side fixture at `caixa-helm/src/lib.rs:978` /
11584 // `caixa-flux/src/lib.rs:2319` / `caixa-mesh/src/lib.rs:3208`
11585 // carries by construction), `Some("2018")` / `Some("2021")` /
11586 // `Some("2024")` (canonical 4-digit-ASCII-decimal-year shapes
11587 // peer with Cargo's `[package] edition` grammar every future-
11588 // introduced sibling to `"2026"` will follow), and eight
11589 // past-the-guard sentinels for the `EdicaoInvalid` refusal cases
11590 // (`Some("2026 ")` trailing-whitespace, `Some(" 2026")` leading-
11591 // whitespace, `Some("2026\n")` embedded-LF, `Some("2026")`
11592 // fullwidth-non-ASCII-lookalike, `Some("v2026")` version-tag-
11593 // prefix, `Some("2026.1")` decimal-shape, `Some("26")` wrong-
11594 // length-numeric, `Some("latest")` free-form-non-year — the
11595 // sentinels pin the accessor doesn't silently absorb the
11596 // refusal cases into a substrate-default-edition fallback).
11597 //
11598 // Fourth and final outer top-level [`Caixa`] `Option<&str>`-
11599 // return scalar accessor pin on the substrate primitive —
11600 // sibling of the peer [`Caixa::licenca`] (6d5bc28),
11601 // [`Caixa::repositorio`] (cc7332d), and [`Caixa::descricao`]
11602 // (3f16e2f) pins that opened the "outer [`Caixa`]
11603 // `Option<&str>` scalar" projection pin pattern this pin folds
11604 // on. Sibling in shape to the peer per-`:placement`
11605 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
11606 // [`crate::aplicacao::Placement::affinity`] (74ec2d3) accessor
11607 // pins on the sibling per-M3-mesh-slot `Option<&str>`-return
11608 // axes, extended onto the outer top-level [`Caixa`] universal-
11609 // axis surface's last unlifted `Option<String>` slot. Pins
11610 // against a future silent detour that returned an owned
11611 // `Option<String>` (which would type-check but silently
11612 // allocate on every accessor call, breaking the zero-cost
11613 // projection every peer sibling accessor carries), a
11614 // `Some("") → None` collapse (which would silently absorb the
11615 // `EdicaoEmpty` refusal case at the accessor boundary and any
11616 // future edition-aware consumer would silently fall back to
11617 // the substrate's default edition on a struct-literal
11618 // `Caixa { edicao: Some(""), .. }`), or a
11619 // `None → Some("2026")` collapse (which would silently reify
11620 // the substrate's default edition at the accessor boundary
11621 // and every downstream consumer keying off the
11622 // `Option::is_none()` discriminator would lose the "author
11623 // omitted the slot" signal).
11624 for edicao in [
11625 None,
11626 Some(""),
11627 Some("2026"),
11628 Some("2018"),
11629 Some("2021"),
11630 Some("2024"),
11631 Some("2026 "),
11632 Some(" 2026"),
11633 Some("2026\n"),
11634 Some("2026"),
11635 Some("v2026"),
11636 Some("2026.1"),
11637 Some("26"),
11638 Some("latest"),
11639 ] {
11640 let c = caixa_with_edicao(edicao);
11641 assert_eq!(
11642 c.edicao(),
11643 edicao,
11644 "Caixa::edicao must return :edicao verbatim (got {:?}, \
11645 expected {edicao:?})",
11646 c.edicao(),
11647 );
11648 assert_eq!(
11649 c.edicao(),
11650 c.edicao.as_deref(),
11651 "Caixa::edicao must byte-equal the raw \
11652 `self.edicao.as_deref()` field access across every \
11653 value in the Option<&str> accept-set",
11654 );
11655 }
11656 }
11657
11658 #[test]
11659 fn validate_edicao_empty_arm_routes_through_accessor() {
11660 // Composition pin: [`Caixa::validate_edicao`]'s empty-arm gate
11661 // must key off [`Caixa::edicao`], not the raw
11662 // `self.edicao.as_deref()` field access. Structurally: a
11663 // `Caixa { edicao: Some(""), .. }` must surface the
11664 // `EdicaoEmpty` refusal exactly, and a
11665 // `Caixa { edicao: Some("2026"), .. }` (the canonical
11666 // 4-digit-ASCII-decimal-year form) must pass validate. The
11667 // pair jointly pins the accessor + validate-gate composition:
11668 // any future silent detour that had the accessor return `None`
11669 // on the empty arm (a `.filter(|s| !s.is_empty())` collapse)
11670 // would silently absorb the `EdicaoEmpty` refusal at the
11671 // accessor boundary and the validate gate would accept a
11672 // struct-literal `Caixa { edicao: Some(""), .. }` — the
11673 // composition pin catches that at caixa-core build time.
11674 //
11675 // Peer of the [`Caixa::licenca`] (6d5bc28)
11676 // `validate_licenca_empty_arm_routes_through_accessor`,
11677 // [`Caixa::repositorio`] (cc7332d)
11678 // `validate_repositorio_empty_arm_routes_through_accessor`,
11679 // and [`Caixa::descricao`] (3f16e2f)
11680 // `validate_descricao_empty_arm_routes_through_accessor`
11681 // composition pins on the sibling outer top-level [`Caixa`]
11682 // `Option<&str>` universal-axis surface — same "the validate /
11683 // shape-gate predicate must route through the substrate-
11684 // primitive typed dispatch" discipline extended onto the
11685 // fourth and final outer top-level [`Caixa`] universal-axis
11686 // `Option<&str>`-composition surface, closing the accessor-
11687 // composition family.
11688 let c = caixa_with_edicao(Some(""));
11689 assert!(
11690 matches!(c.validate_edicao(), Err(ManifestError::EdicaoEmpty)),
11691 "validate_edicao must reject edicao == Some(\"\") with \
11692 EdicaoEmpty — the accessor and the validate gate must \
11693 route through the same substrate-primitive typed dispatch \
11694 on the :edicao empty arm",
11695 );
11696 let c = caixa_with_edicao(Some("2026"));
11697 assert!(
11698 c.validate_edicao().is_ok(),
11699 "validate_edicao must accept edicao == Some(\"2026\") \
11700 (the canonical 4-digit-ASCII-decimal-year shape)",
11701 );
11702 }
11703
11704 #[test]
11705 fn edicao_projects_option_str_by_borrow() {
11706 // The by-borrow pin: [`Caixa::edicao`] returns
11707 // `Option<&str>` by borrow — the `&str` borrows the underlying
11708 // `String` storage of the `Option<String>` slot and the
11709 // accessor must not allocate a fresh `String` on every call.
11710 // Peer of the [`Caixa::licenca`] (6d5bc28),
11711 // [`Caixa::repositorio`] (cc7332d), and [`Caixa::descricao`]
11712 // (3f16e2f) by-borrow pins on the peer outer top-level
11713 // [`Caixa`] `Option<&str>`-return axes, and of the
11714 // per-`:placement`
11715 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) by-
11716 // borrow pin on the peer per-M3-mesh-slot `Option<&str>`-
11717 // return axis, extended onto the fourth and final outer top-
11718 // level [`Caixa`] universal-axis `Option<&str>` shape — the
11719 // accessor's returned `&str` must borrow from `&self` (the
11720 // returned reference's lifetime is tied to `&self`), and
11721 // calling the accessor twice on the same [`Caixa`] must yield
11722 // the same `Option<&str>` verbatim (idempotent, no side
11723 // effects on `&self`).
11724 //
11725 // Pins against a future silent detour that returned an owned
11726 // `Option<String>` (which would type-check but silently
11727 // allocate on every call, breaking the zero-cost projection
11728 // every peer sibling accessor carries), or a one-arm-only
11729 // accessor that returned a saturating value on some sentinel
11730 // input (breaking the pass-through invariant the sibling
11731 // required-scalar accessors carry).
11732 for edicao in [None, Some(""), Some("2026"), Some("2018")] {
11733 let c = caixa_with_edicao(edicao);
11734 let first = c.edicao();
11735 let second = c.edicao();
11736 assert_eq!(
11737 first, second,
11738 "Caixa::edicao must be idempotent — two successive \
11739 calls on the same &self must return the same \
11740 Option<&str>",
11741 );
11742 assert_eq!(
11743 first, edicao,
11744 "Caixa::edicao must return :edicao verbatim by \
11745 borrow — got {first:?}, expected {edicao:?}",
11746 );
11747 }
11748 }
11749
11750 #[test]
11751 fn nome_returns_nome_byte_string_verbatim_across_permutations() {
11752 // The canonical per-`Caixa` `:nome` universal-axis DNS-1123-
11753 // label caixa-identity scalar pin: [`Caixa::nome`] must return
11754 // the `:nome` typed `String` verbatim as `&str`, byte-equal to
11755 // the raw field access across every representative value in
11756 // the accept-set — the canonical `"demo"` template baseline
11757 // (the same `feira init`-scaffolded default the sibling
11758 // `validate_nome_accepts_canonical_template` positive-control
11759 // gate pins), plus every sibling per-typed-slot atom accessor's
11760 // canonical positive-arm byte-string (`"catalog"` per
11761 // [`crate::aplicacao::Membro::nome`], `"cart"` per the peer
11762 // per-`:contratos` `:de`, `"hello-rio"` per the canonical
11763 // `caixa-helm`/`caixa-flux` cross-crate integration-test
11764 // fixture, `"checkout"` per the M3 mesh-slot Aplicacao
11765 // canonical example), plus every past-the-guard sentinel for
11766 // the `NomeEmpty` / `NomeInvalid` / `NomeChartNameBudgetExceeded`
11767 // refusal cases (`""`, `"Bad_Name"`, `"a"` × 56 — 56 bytes fits
11768 // the bare DNS-1123 63-byte cap but overflows the joint
11769 // `lareira-<nome>` chart-name budget the sibling
11770 // [`Caixa::validate_nome_chart_name_budget`] gate closes on).
11771 //
11772 // The past-the-guard sentinels pin the accessor doesn't
11773 // silently absorb the refusal cases into a template-derived
11774 // fallback (a future `.nome().is_empty().then(|| "demo")`
11775 // collapse would silently absorb the `NomeEmpty` refusal at
11776 // the accessor boundary and the validate gate would accept a
11777 // struct-literal `Caixa { nome: "".into(), .. }` — the pin
11778 // catches that at caixa-core build time).
11779 //
11780 // First outer top-level [`Caixa`] `&str`-return required-
11781 // scalar accessor pin — opens the "outer [`Caixa`] `&str`
11782 // required-scalar" projection pattern the sibling per-`Caixa`
11783 // `:versao` future lift folds on. Sibling in shape to the peer
11784 // per-`:membros` [`crate::aplicacao::Membro::nome`] (4a32abf)
11785 // required-`String`-carry accessor pin on the sibling per-
11786 // sub-struct required-axis, extended onto the outer top-level
11787 // [`Caixa`] universal-axis required-`String`-carry axis.
11788 for nome in [
11789 "demo",
11790 "catalog",
11791 "cart",
11792 "hello-rio",
11793 "checkout",
11794 "",
11795 "Bad_Name",
11796 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
11797 ] {
11798 let c = caixa_with_nome(nome);
11799 assert_eq!(
11800 c.nome(),
11801 nome,
11802 "Caixa::nome must return :nome verbatim (got {}, \
11803 expected {nome})",
11804 c.nome(),
11805 );
11806 assert_eq!(
11807 c.nome(),
11808 c.nome.as_str(),
11809 "Caixa::nome must byte-equal the raw .nome field \
11810 access across every value in the String accept-set",
11811 );
11812 }
11813 }
11814
11815 #[test]
11816 fn validate_nome_empty_arm_routes_through_accessor() {
11817 // Composition pin: [`Caixa::validate_nome`]'s empty-arm must
11818 // key off [`Caixa::nome`], not the raw `.nome` field access.
11819 // Structurally: a `Caixa { nome: "".into(), .. }` must surface
11820 // the `NomeEmpty` refusal exactly, and the canonical `"demo"`
11821 // template baseline (the peer positive-arm the sibling
11822 // `validate_nome_accepts_canonical_template` gate carves out)
11823 // must pass validate. The pair jointly pins the accessor +
11824 // validate-gate composition: any future silent detour that
11825 // had the accessor return a fresh `"demo"` on the empty arm
11826 // (a `.nome().is_empty().then(|| "demo")` fallback collapse)
11827 // would silently absorb the `NomeEmpty` refusal at the
11828 // accessor boundary and the validate gate would accept a
11829 // struct-literal `Caixa { nome: "".into(), .. }` — the
11830 // composition pin catches that at caixa-core build time.
11831 //
11832 // Peer of the sibling per-`Caixa`
11833 // `validate_licenca_empty_arm_routes_through_accessor` (6d5bc28)
11834 // / `validate_repositorio_empty_arm_routes_through_accessor`
11835 // (cc7332d) / `validate_descricao_empty_arm_routes_through_accessor`
11836 // (3f16e2f) / `validate_edicao_empty_arm_routes_through_accessor`
11837 // (2641cbd) composition pins on the sibling outer top-level
11838 // [`Caixa`] `Option<&str>` axes — same "the validate /
11839 // shape-gate predicate must route through the substrate-
11840 // primitive typed dispatch" discipline extended onto the peer
11841 // outer top-level [`Caixa`] required-`&str` composition axis.
11842 let c = caixa_with_nome("");
11843 assert!(
11844 matches!(c.validate_nome(), Err(ManifestError::NomeEmpty)),
11845 "validate_nome must reject nome == \"\" with NomeEmpty — \
11846 the accessor and the validate gate must route through the \
11847 same substrate-primitive typed dispatch on the :nome \
11848 empty-arm",
11849 );
11850 let c = caixa_with_nome("demo");
11851 assert!(
11852 c.validate_nome().is_ok(),
11853 "validate_nome must accept nome == \"demo\" (the canonical \
11854 DNS-1123-label template baseline)",
11855 );
11856 }
11857
11858 #[test]
11859 fn nome_projects_str_by_borrow() {
11860 // The by-borrow pin: [`Caixa::nome`] returns `&str` by borrow
11861 // — the `&str` borrows the underlying `String` storage of the
11862 // required `nome` slot and the accessor must not allocate a
11863 // fresh `String` on every call. Peer of the [`Caixa::licenca`]
11864 // (6d5bc28) / [`Caixa::repositorio`] (cc7332d) /
11865 // [`Caixa::descricao`] (3f16e2f) / [`Caixa::edicao`] (2641cbd)
11866 // by-borrow pins on the peer outer top-level [`Caixa`]
11867 // `Option<&str>`-return axes, extended onto the first outer
11868 // top-level [`Caixa`] required-`&str`-return axis — the
11869 // accessor's returned `&str` must borrow from `&self` (the
11870 // returned reference's lifetime is tied to `&self`), and
11871 // calling the accessor twice on the same [`Caixa`] must yield
11872 // the same `&str` verbatim (idempotent, no side effects on
11873 // `&self`).
11874 //
11875 // Pins against a future silent detour that returned an owned
11876 // `String` (which would type-check but silently allocate on
11877 // every call, breaking the zero-cost projection every peer
11878 // sibling accessor carries), an accidental
11879 // `.nome.to_lowercase()` detour that returned a fresh
11880 // allocation through an already-DNS-1123-lowercase-only
11881 // string (breaking a future `const fn` regression), or a
11882 // one-arm-only accessor that returned a canonicalized value
11883 // on some sentinel input (breaking the pass-through invariant
11884 // the sibling required-scalar accessors carry).
11885 for nome in ["demo", "catalog", "hello-rio", "checkout"] {
11886 let c = caixa_with_nome(nome);
11887 let first = c.nome();
11888 let second = c.nome();
11889 assert_eq!(
11890 first, second,
11891 "Caixa::nome must be idempotent — two successive calls \
11892 on the same &self must return the same &str",
11893 );
11894 assert_eq!(
11895 first, nome,
11896 "Caixa::nome must return :nome verbatim by borrow — \
11897 got {first}, expected {nome}",
11898 );
11899 }
11900 }
11901
11902 #[test]
11903 fn versao_returns_versao_byte_string_verbatim_across_permutations() {
11904 // The canonical per-`Caixa` `:versao` universal-axis SemVer-2
11905 // pinned-version scalar pin: [`Caixa::versao`] must return the
11906 // `:versao` typed `String` verbatim as `&str`, byte-equal to the
11907 // raw `.versao` field access across every representative value
11908 // in the accept-set — the canonical `"0.1.0"` template baseline
11909 // (the same `feira init`-scaffolded default the sibling
11910 // `validate_versao_accepts_canonical_template` positive-control
11911 // gate pins), plus every canonical SemVer-2 shape the sibling
11912 // `validate_versao_accepts_canonical_forms` positive-arm sweep
11913 // covers (`"0.0.0"`, `"1.0.0"`, `"0.2.0-rc.1"`,
11914 // `"1.0.0-alpha.0"`, `"1.0.0+build.42"`, `"1.0.0-rc.1+build.42"`,
11915 // `"10.20.30"`), plus every past-the-guard sentinel for the
11916 // `VersaoEmpty` / `VersaoInvalid` refusal cases (`""` the empty
11917 // arm, `"v0.1.0"` the git-tag-shape-leak footgun, `"0.1"` the
11918 // missing-patch footgun, `"^0.1"` the requirement-shape-leak
11919 // footgun, `"0.1.0.0"` the four-part-Java-convention footgun,
11920 // `"latest"` the docker-tag-shape footgun — the sentinels pin
11921 // the accessor doesn't silently absorb the refusal cases into a
11922 // template-derived fallback like `"0.1.0"`).
11923 //
11924 // The past-the-guard sentinels pin the accessor doesn't silently
11925 // absorb the refusal cases into a template-derived fallback (a
11926 // future `.versao().is_empty().then(|| "0.1.0")` collapse would
11927 // silently absorb the `VersaoEmpty` refusal at the accessor
11928 // boundary and the validate gate would accept a struct-literal
11929 // `Caixa { versao: "".into(), .. }` — the pin catches that at
11930 // caixa-core build time).
11931 //
11932 // Second outer top-level [`Caixa`] `&str`-return required-scalar
11933 // accessor pin — folds on the "outer [`Caixa`] `&str` required-
11934 // scalar" projection pattern the sibling per-`Caixa`
11935 // [`Caixa::nome`] (e6b7d97) opened. Sibling in shape to the peer
11936 // per-`:membros` [`crate::aplicacao::Membro::versao_requirement`]
11937 // (4127bb6) / per-`:children`
11938 // [`crate::supervisor::ChildSpec::versao_requirement`] (2c053c8)
11939 // / per-`:upgrade-from`
11940 // [`crate::UpgradeFromEntry::prior_versao`] (75d27a8) per-sub-
11941 // struct `:versao`-shaped `&str`-return accessor pins on the
11942 // sibling per-typed-slot version-carrier axes, extended onto the
11943 // second outer top-level [`Caixa`] universal-axis required-
11944 // `String`-carry axis so the two universal-axis identity-
11945 // carrying scalars every `defcaixa` form supplies (`:nome` +
11946 // `:versao`) share the same "one typed dispatch per axis" pin
11947 // discipline.
11948 for versao in [
11949 "0.1.0",
11950 "0.0.0",
11951 "1.0.0",
11952 "0.2.0-rc.1",
11953 "1.0.0-alpha.0",
11954 "1.0.0+build.42",
11955 "1.0.0-rc.1+build.42",
11956 "10.20.30",
11957 "",
11958 "v0.1.0",
11959 "0.1",
11960 "^0.1",
11961 "0.1.0.0",
11962 "latest",
11963 ] {
11964 let c = caixa_with_versao(versao);
11965 assert_eq!(
11966 c.versao(),
11967 versao,
11968 "Caixa::versao must return :versao verbatim (got {}, \
11969 expected {versao})",
11970 c.versao(),
11971 );
11972 assert_eq!(
11973 c.versao(),
11974 c.versao.as_str(),
11975 "Caixa::versao must byte-equal the raw .versao field \
11976 access across every value in the String accept-set",
11977 );
11978 }
11979 }
11980
11981 #[test]
11982 fn validate_versao_empty_arm_routes_through_accessor() {
11983 // Composition pin: [`Caixa::validate_versao`]'s empty-arm gate
11984 // must key off [`Caixa::versao`], not the raw `.versao` field
11985 // access. Structurally: a `Caixa { versao: "".into(), .. }` must
11986 // surface the `VersaoEmpty` refusal exactly, and the canonical
11987 // `"0.1.0"` template baseline (the peer positive-arm the sibling
11988 // `validate_versao_accepts_canonical_template` gate carves out)
11989 // must pass validate. The pair jointly pins the accessor +
11990 // validate-gate composition: any future silent detour that had
11991 // the accessor return a fresh `"0.1.0"` on the empty arm
11992 // (a `.versao().is_empty().then(|| "0.1.0")` fallback collapse)
11993 // would silently absorb the `VersaoEmpty` refusal at the
11994 // accessor boundary and the validate gate would accept a
11995 // struct-literal `Caixa { versao: "".into(), .. }` — the
11996 // composition pin catches that at caixa-core build time.
11997 //
11998 // Peer of the sibling per-`Caixa`
11999 // `validate_nome_empty_arm_routes_through_accessor` (e6b7d97)
12000 // composition pin on the sibling outer top-level [`Caixa`]
12001 // required-`&str` universal-axis surface — same "the validate /
12002 // shape-gate predicate must route through the substrate-
12003 // primitive typed dispatch" discipline extended onto the peer
12004 // outer top-level [`Caixa`] required-`&str` universal-axis
12005 // pinned-version composition axis, closing the second
12006 // coordinate of the "one canonical typed dispatch per per-Caixa
12007 // required-`&str` universal-axis" discipline.
12008 let c = caixa_with_versao("");
12009 assert!(
12010 matches!(c.validate_versao(), Err(ManifestError::VersaoEmpty)),
12011 "validate_versao must reject versao == \"\" with VersaoEmpty — \
12012 the accessor and the validate gate must route through the \
12013 same substrate-primitive typed dispatch on the :versao \
12014 empty-arm",
12015 );
12016 let c = caixa_with_versao("0.1.0");
12017 assert!(
12018 c.validate_versao().is_ok(),
12019 "validate_versao must accept versao == \"0.1.0\" (the \
12020 canonical SemVer-2 template baseline)",
12021 );
12022 }
12023
12024 #[test]
12025 fn versao_projects_str_by_borrow() {
12026 // The by-borrow pin: [`Caixa::versao`] returns `&str` by borrow
12027 // — the `&str` borrows the underlying `String` storage of the
12028 // required `versao` slot and the accessor must not allocate a
12029 // fresh `String` on every call. Peer of the [`Caixa::nome`]
12030 // (e6b7d97) by-borrow pin on the sibling outer top-level
12031 // [`Caixa`] required-`&str`-return axis, extended onto the
12032 // second outer top-level [`Caixa`] required-`&str`-return
12033 // universal-axis pinned-version surface — the accessor's
12034 // returned `&str` must borrow from `&self` (the returned
12035 // reference's lifetime is tied to `&self`), and calling the
12036 // accessor twice on the same [`Caixa`] must yield the same
12037 // `&str` verbatim (idempotent, no side effects on `&self`).
12038 //
12039 // Pins against a future silent detour that returned an owned
12040 // `String` (which would type-check but silently allocate on
12041 // every call, breaking the zero-cost projection every peer
12042 // sibling accessor carries), an accidental
12043 // `semver::Version::parse(&self.versao).unwrap().to_string()`
12044 // detour that returned a canonicalized fresh allocation through
12045 // an already-canonical byte-string (breaking a future `const fn`
12046 // regression and silently absorbing the `VersaoInvalid` refusal
12047 // at the accessor boundary), or a one-arm-only accessor that
12048 // returned a canonicalized value on some sentinel input
12049 // (breaking the pass-through invariant the sibling required-
12050 // scalar accessors carry).
12051 for versao in ["0.1.0", "1.0.0", "0.2.0-rc.1", "1.0.0+build.42"] {
12052 let c = caixa_with_versao(versao);
12053 let first = c.versao();
12054 let second = c.versao();
12055 assert_eq!(
12056 first, second,
12057 "Caixa::versao must be idempotent — two successive \
12058 calls on the same &self must return the same &str",
12059 );
12060 assert_eq!(
12061 first, versao,
12062 "Caixa::versao must return :versao verbatim by borrow \
12063 — got {first}, expected {versao}",
12064 );
12065 }
12066 }
12067
12068 fn caixa_with_kind(kind: CaixaKind) -> Caixa {
12069 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
12070 c.kind = kind;
12071 c
12072 }
12073
12074 #[test]
12075 fn kind_returns_kind_variant_verbatim_across_permutations() {
12076 // The canonical per-`Caixa` `:kind` universal-axis closed-set-
12077 // enum discriminant pin: [`Caixa::kind`] must return the `:kind`
12078 // typed [`CaixaKind`] variant verbatim by `Copy`, byte-equal to
12079 // the raw `.kind` field access across every variant in the
12080 // closed accept-set (`Biblioteca` — the library kind that
12081 // exports lisp forms; `Binario` — the nix-built executable kind
12082 // under `exe/`; `Servico` — the wasm-component daemon kind
12083 // under `servicos/`; `Supervisor` — the OTP-shaped hierarchical
12084 // reconciliation kind; `Aplicacao` — the M3 typed-mesh
12085 // composition kind).
12086 //
12087 // Pins against a future silent detour that re-derived the kind
12088 // from a peer axis (an accidental fallback to
12089 // `if !servicos.is_empty() { Servico } else if
12090 // !membros.is_empty() { Aplicacao } else { Biblioteca }`
12091 // collapse that read the code-surface / mesh-slot columns into
12092 // the kind discriminator), a variant remap the operator
12093 // authors on one consumer without the other, or a stale-derive
12094 // detour that substituted [`CaixaKind::Biblioteca`] as the
12095 // default when the field held any other variant (which would
12096 // silently collapse the distinction between "author explicitly
12097 // declared `:kind Servico`" and "author declared any other
12098 // kind" every downstream renderer-dispatch site depends on).
12099 //
12100 // First outer top-level [`Caixa`] `Copy`-return required-enum-
12101 // discriminant accessor pin — opens the "outer [`Caixa`]
12102 // `Copy`-return required-discriminant" projection pattern.
12103 // Sibling in shape to the peer per-`:supervisor`
12104 // [`crate::supervisor::SupervisorSpec::estrategia`] (eafb619),
12105 // per-`:placement` [`crate::aplicacao::Placement::estrategia`]
12106 // (921fe1b), and per-`:children`
12107 // [`crate::supervisor::ChildSpec::restart`] (dfb4a81)
12108 // `Copy`-return closed-set-enum discriminant accessor pins on
12109 // the sibling nested-spec typed-slot discriminator axes,
12110 // extended here to the outer top-level [`Caixa`] universal-
12111 // axis surface.
12112 for kind in [
12113 CaixaKind::Biblioteca,
12114 CaixaKind::Binario,
12115 CaixaKind::Servico,
12116 CaixaKind::Supervisor,
12117 CaixaKind::Aplicacao,
12118 ] {
12119 let c = caixa_with_kind(kind);
12120 assert_eq!(
12121 c.kind(),
12122 kind,
12123 "Caixa::kind must return :kind verbatim (got {:?}, \
12124 expected {kind:?})",
12125 c.kind(),
12126 );
12127 assert_eq!(
12128 c.kind(),
12129 c.kind,
12130 "Caixa::kind accessor and .kind field access must \
12131 byte-equal — the accessor is the substrate-primitive \
12132 typed dispatch every downstream kind-gate consumer \
12133 must route through",
12134 );
12135 }
12136 }
12137
12138 #[test]
12139 fn require_kind_reads_through_lifted_kind_accessor() {
12140 // Two-consumer coherence pin: the [`crate::render::require_kind`]
12141 // entry-gate predicate (the canonical two-line
12142 // `require_kind(caixa, Servico)?` prelude every per-Servico /
12143 // per-Aplicacao renderer runs at its entry-point) and the
12144 // sibling [`crate::render::KindMismatch`] error carrier's
12145 // `actual:` field (which names the offending caixa's variant
12146 // in the diagnostic) must both key off the lifted accessor, so
12147 // any future rebrand on the typed slot's reader shape lands at
12148 // exactly one place. Pins the two-site coherence by exercising
12149 // every off-diagonal `(actual, expected)` pair across the
12150 // closed accept-set — the `KindMismatch { actual, expected }`
12151 // surfaced on the mismatch arm must byte-equal the pair the
12152 // accessor returns for each side.
12153 //
12154 // Peer of the sibling per-`:placement`
12155 // `validate_placement_reads_through_lifted_estrategia_accessor`
12156 // (921fe1b) two-arm consumer-coherence pin on the M3 mesh-slot
12157 // `Copy`-return discriminant axis — same "the entry-gate
12158 // predicate and the error carrier's `actual:` field must route
12159 // through the substrate-primitive typed dispatch" discipline
12160 // extended onto the outer top-level [`Caixa`] universal-axis
12161 // discriminant surface.
12162 for expected in [
12163 CaixaKind::Biblioteca,
12164 CaixaKind::Binario,
12165 CaixaKind::Servico,
12166 CaixaKind::Supervisor,
12167 CaixaKind::Aplicacao,
12168 ] {
12169 for actual in [
12170 CaixaKind::Biblioteca,
12171 CaixaKind::Binario,
12172 CaixaKind::Servico,
12173 CaixaKind::Supervisor,
12174 CaixaKind::Aplicacao,
12175 ] {
12176 let c = caixa_with_kind(actual);
12177 let result = crate::render::require_kind(&c, expected);
12178 if expected == actual {
12179 assert!(
12180 result.is_ok(),
12181 "require_kind must accept when actual == expected \
12182 (actual={actual:?}, expected={expected:?})",
12183 );
12184 } else {
12185 let err = result.expect_err("require_kind must reject when actual != expected");
12186 assert_eq!(
12187 err.actual,
12188 c.kind(),
12189 "KindMismatch.actual must byte-equal Caixa::kind() \
12190 — the error carrier's `actual:` field reads \
12191 through the lifted accessor",
12192 );
12193 assert_eq!(
12194 err.expected, expected,
12195 "KindMismatch.expected must byte-equal the \
12196 expected variant passed to require_kind",
12197 );
12198 }
12199 }
12200 }
12201 }
12202
12203 #[test]
12204 fn aplicacao_view_kind_gate_routes_through_accessor() {
12205 // Composition pin: [`Caixa::aplicacao_view`]'s kind-gate arm
12206 // must key off [`Caixa::kind`], not the raw `.kind` field
12207 // access. Structurally: a `Caixa { kind: X, .. }` for any
12208 // non-`Aplicacao` variant must fold to `None` on the
12209 // `aplicacao_view` composer (the "kind mismatch → no typed
12210 // view" contract every downstream Aplicacao consumer keys off
12211 // via `?`), and a `Caixa { kind: Aplicacao, .. }` must fold to
12212 // `Some(_)`. The pair jointly pins the accessor + view-gate
12213 // composition: any future silent detour that had the accessor
12214 // return a fresh [`CaixaKind::Aplicacao`] on some sentinel
12215 // input would silently absorb the kind-mismatch case at the
12216 // accessor boundary and every per-Aplicacao renderer would
12217 // silently render a non-Aplicacao caixa's mesh slots — the
12218 // composition pin catches that at caixa-core build time.
12219 //
12220 // Peer of the sibling per-`Caixa`
12221 // `validate_nome_empty_arm_routes_through_accessor` (e6b7d97) /
12222 // `validate_versao_empty_arm_routes_through_accessor` (20c0539)
12223 // composition pins on the sibling outer top-level [`Caixa`]
12224 // required-`&str` universal-axis surfaces — same "the
12225 // composer / validate gate must route through the substrate-
12226 // primitive typed dispatch" discipline extended onto the
12227 // outer top-level [`Caixa`] `Copy`-return required-
12228 // discriminant composition axis.
12229 for kind in [
12230 CaixaKind::Biblioteca,
12231 CaixaKind::Binario,
12232 CaixaKind::Servico,
12233 CaixaKind::Supervisor,
12234 ] {
12235 let c = caixa_with_kind(kind);
12236 assert!(
12237 c.aplicacao_view().is_none(),
12238 "aplicacao_view must return None on non-Aplicacao \
12239 kind {kind:?} — the composer's kind-gate must route \
12240 through Caixa::kind()",
12241 );
12242 }
12243 let c = caixa_with_kind(CaixaKind::Aplicacao);
12244 assert!(
12245 c.aplicacao_view().is_some(),
12246 "aplicacao_view must return Some on kind Aplicacao — \
12247 the composer's kind-gate must accept the matching arm \
12248 through Caixa::kind()",
12249 );
12250 }
12251
12252 #[test]
12253 fn supervisor_view_kind_gate_routes_through_accessor() {
12254 // Composition pin (mirror of the sibling
12255 // `aplicacao_view_kind_gate_routes_through_accessor` on the
12256 // second `_view` composer): [`Caixa::supervisor_view`]'s kind-
12257 // gate arm must key off [`Caixa::kind`], not the raw `.kind`
12258 // field access. A `Caixa { kind: X, .. }` for any non-
12259 // `Supervisor` variant must fold to `None` on the
12260 // `supervisor_view` composer, and a `Caixa { kind:
12261 // Supervisor, .. }` must fold to `Some(_)`. Same peer
12262 // composition pin discipline on the second `_view` composer
12263 // axis.
12264 for kind in [
12265 CaixaKind::Biblioteca,
12266 CaixaKind::Binario,
12267 CaixaKind::Servico,
12268 CaixaKind::Aplicacao,
12269 ] {
12270 let c = caixa_with_kind(kind);
12271 assert!(
12272 c.supervisor_view().is_none(),
12273 "supervisor_view must return None on non-Supervisor \
12274 kind {kind:?} — the composer's kind-gate must route \
12275 through Caixa::kind()",
12276 );
12277 }
12278 let mut c = caixa_with_kind(CaixaKind::Supervisor);
12279 // A Supervisor caixa needs a strategy + at least one child to
12280 // fold to a Some(_) that also validates; the composer itself
12281 // requires only the kind arm, so bare kind flip is enough to
12282 // pin the `Some(_)` return, but we populate the minimum
12283 // supervisor shape so a future strengthening of the composer
12284 // to reject an empty spec doesn't false-positive this pin.
12285 c.estrategia = Some(crate::supervisor::RestartStrategy::OneForOne);
12286 c.children = vec![crate::supervisor::ChildSpec {
12287 caixa: "child".into(),
12288 versao: "^0.1".into(),
12289 restart: crate::supervisor::RestartPolicy::Permanent,
12290 }];
12291 assert!(
12292 c.supervisor_view().is_some(),
12293 "supervisor_view must return Some on kind Supervisor — \
12294 the composer's kind-gate must accept the matching arm \
12295 through Caixa::kind()",
12296 );
12297 }
12298
12299 #[test]
12300 fn kind_projects_by_copy() {
12301 // The by-`Copy` pin: [`Caixa::kind`] returns a fresh
12302 // [`CaixaKind`] by `Copy` — the accessor must not borrow from
12303 // `&self` (the returned value is owned, `Copy`-projected from
12304 // the underlying [`CaixaKind`] storage; two calls on the same
12305 // [`Caixa`] must yield byte-equal values). Peer of the peer
12306 // per-`:placement` `Placement::estrategia` / per-`:supervisor`
12307 // `SupervisorSpec::estrategia` / per-`:children`
12308 // `ChildSpec::restart` `Copy`-return discriminant accessor
12309 // pins on the sibling nested-spec typed-slot discriminator
12310 // axes, extended onto the first outer top-level [`Caixa`]
12311 // required-`Copy`-return axis — pins against a future silent
12312 // detour that returned `&CaixaKind` (which would type-check
12313 // but silently constrain every consumer's callsite to a
12314 // borrow-shaped dispatch, breaking the zero-cost `Copy`
12315 // projection every peer sibling accessor carries).
12316 for kind in [
12317 CaixaKind::Biblioteca,
12318 CaixaKind::Binario,
12319 CaixaKind::Servico,
12320 CaixaKind::Supervisor,
12321 CaixaKind::Aplicacao,
12322 ] {
12323 let c = caixa_with_kind(kind);
12324 let first: CaixaKind = c.kind();
12325 let second: CaixaKind = c.kind();
12326 assert_eq!(
12327 first, second,
12328 "Caixa::kind must be idempotent — two successive \
12329 calls on the same &self must return the same \
12330 CaixaKind variant",
12331 );
12332 assert_eq!(
12333 first, kind,
12334 "Caixa::kind must return :kind verbatim by Copy — \
12335 got {first:?}, expected {kind:?}",
12336 );
12337 }
12338 }
12339
12340 // ── Caixa::autores — outer top-level &[T] slice accessor ──────────
12341
12342 #[test]
12343 fn autores_returns_autores_slice_verbatim_across_permutations() {
12344 // The canonical per-`Caixa` `:autores` universal-axis maintainer-
12345 // name-list slice pin: [`Caixa::autores`] must return the
12346 // `:autores` typed [`Vec<String>`] list verbatim as a
12347 // `&[String]`, byte-equal to the raw `self.autores.as_slice()`
12348 // access across every representative value in the accept-set —
12349 // `[]` (the "no maintainers declared" arm every existing
12350 // fixture without an `:autores` line carries), `[""]` (a past-
12351 // the-guard sentinel that pins the accessor doesn't perform a
12352 // silent `[""] → []` collapse on the empty-entry arm — validate
12353 // rejects `[""]` through `AutorEmpty` but the accessor must
12354 // ship the raw slot verbatim so a validate-time gate regression
12355 // surfaces at the caixa-helm emit boundary rather than being
12356 // silently absorbed into a maintainer-drop), `["pleme-io"]` (the
12357 // canonical single-maintainer form every `feira init` template
12358 // scaffolds), `["alice", "bob"]` (a canonical multi-maintainer
12359 // form), `["alice <alice@example.com>", "bob <bob@example.com>"]`
12360 // (the canonical RFC-5322 `<name> <email>` form the
12361 // `is_chart_maintainer_name_shape` predicate accepts), and
12362 // `["pleme-io", "pleme-io"]` (a past-the-guard duplicate
12363 // sentinel — validate rejects through `AutorDuplicate` but the
12364 // accessor must ship the raw slot verbatim).
12365 //
12366 // First outer top-level [`Caixa`] `&[T]`-return slice accessor
12367 // pin on the substrate primitive — opens the "outer [`Caixa`]
12368 // `&[T]` slice" projection pattern the sibling per-`Caixa`
12369 // `:etiquetas` / `:deps` / `:deps-dev` / `:exe` / `:bibliotecas`
12370 // / `:servicos` / `:upgrade-from` / `:children` future lifts
12371 // fold on. Sibling in shape to the peer per-`:supervisor`
12372 // [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
12373 // per-`:placement` [`crate::aplicacao::Placement::clusters`]
12374 // (a6e18d7), per-`:membros`
12375 // [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
12376 // per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
12377 // (0dcc926), and per-`:upgrade-from :instructions`
12378 // [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
12379 // `&[T]`-return slice accessor pins on the sibling per-M2 /
12380 // per-M3 typed-slot list axes, extended onto the outer top-
12381 // level [`Caixa`] universal-axis surface. Pins against a future
12382 // silent detour that returned an owned `Vec<String>` (which
12383 // would type-check but silently clone on every accessor call,
12384 // breaking the zero-cost projection every peer sibling slice
12385 // accessor carries), a `[""] → []` collapse (which would
12386 // silently absorb the `AutorEmpty` refusal case at the accessor
12387 // boundary), or a `["a", "a"] → ["a"]` dedup collapse (which
12388 // would silently absorb the `AutorDuplicate` refusal case at
12389 // the accessor boundary and the caixa-helm `maintainers:` fold
12390 // would silently render a dedupped list on a struct-literal
12391 // `Caixa { autores: vec!["a".into(), "a".into()], .. }`).
12392 for autores in [
12393 vec![],
12394 vec![""],
12395 vec!["pleme-io"],
12396 vec!["alice", "bob"],
12397 vec!["alice <alice@example.com>", "bob <bob@example.com>"],
12398 vec!["pleme-io", "pleme-io"],
12399 ] {
12400 let c = caixa_with_autores(autores.clone());
12401 let expected: Vec<String> = autores.iter().map(|s| (*s).to_string()).collect();
12402 assert_eq!(
12403 c.autores(),
12404 expected.as_slice(),
12405 "Caixa::autores must return :autores verbatim (got {:?}, \
12406 expected {expected:?})",
12407 c.autores(),
12408 );
12409 assert_eq!(
12410 c.autores(),
12411 c.autores.as_slice(),
12412 "Caixa::autores must byte-equal the raw \
12413 `self.autores.as_slice()` field access across every \
12414 value in the Vec<String> accept-set",
12415 );
12416 }
12417 }
12418
12419 #[test]
12420 fn validate_autores_empty_entry_arm_routes_through_accessor() {
12421 // Composition pin: [`Caixa::validate_autores`]'s per-entry
12422 // empty-arm gate must key off [`Caixa::autores`], not the raw
12423 // `&self.autores` field-borrow walk. Structurally: a
12424 // `Caixa { autores: vec!["".into()], .. }` must surface the
12425 // `AutorEmpty` refusal exactly, and a
12426 // `Caixa { autores: vec!["pleme-io".into()], .. }` (the
12427 // canonical single-maintainer form) must pass validate. The
12428 // pair jointly pins the accessor + validate-gate composition:
12429 // any future silent detour that had the accessor return an
12430 // empty slice on the `[""]` arm (a
12431 // `.iter().filter(|s| !s.is_empty()).collect()` collapse)
12432 // would silently absorb the `AutorEmpty` refusal at the
12433 // accessor boundary and the validate gate would accept a
12434 // struct-literal `Caixa { autores: vec!["".into()], .. }` —
12435 // the composition pin catches that at caixa-core build time.
12436 //
12437 // Peer of the per-`Caixa` [`Caixa::validate_licenca`] (6d5bc28)
12438 // accessor-composition pin
12439 // (`validate_licenca_empty_arm_routes_through_accessor`) on the
12440 // sibling `Option<&str>`-composition axis and the
12441 // per-`:politicas :circuit-breaker`
12442 // [`crate::aplicacao::CircuitBreaker::max_failures`] (3a74062)
12443 // accessor-composition pin
12444 // (`validate_politicas_max_failures_zero_floor_arm_routes_through_accessor`)
12445 // on the sibling required-`u32`-composition axis — same "the
12446 // validate / shape-gate predicate must route through the
12447 // substrate-primitive typed dispatch" discipline extended onto
12448 // the outer top-level [`Caixa`] universal-axis `&[T]`-
12449 // composition surface.
12450 let c = caixa_with_autores(vec![""]);
12451 assert!(
12452 matches!(c.validate_autores(), Err(ManifestError::AutorEmpty)),
12453 "validate_autores must reject autores == vec![\"\"] with \
12454 AutorEmpty — the accessor and the validate gate must \
12455 route through the same substrate-primitive typed dispatch \
12456 on the :autores per-entry empty arm",
12457 );
12458 let c = caixa_with_autores(vec!["pleme-io"]);
12459 assert!(
12460 c.validate_autores().is_ok(),
12461 "validate_autores must accept autores == vec![\"pleme-io\"] \
12462 (the canonical single-maintainer shape every `feira init` \
12463 template scaffolds)",
12464 );
12465 }
12466
12467 #[test]
12468 fn autores_projects_slice_by_borrow() {
12469 // The by-borrow pin: [`Caixa::autores`] returns `&[String]` by
12470 // borrow — the returned slice borrows the underlying
12471 // `Vec<String>` storage of the `:autores` slot and the
12472 // accessor must not clone the backing `Vec` on every call.
12473 // Peer of the per-`:membros`
12474 // [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36) /
12475 // per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
12476 // (0dcc926) / per-`:placement`
12477 // [`crate::aplicacao::Placement::clusters`] (a6e18d7) /
12478 // per-`:supervisor` [`crate::supervisor::SupervisorSpec::children`]
12479 // (bc92bce) by-borrow pins on the sibling per-M2 / per-M3
12480 // typed-slot `&[T]`-return axes, extended onto the outer top-
12481 // level [`Caixa`] universal-axis `&[String]` shape — the
12482 // accessor's returned slice must borrow from `&self` (the
12483 // returned reference's lifetime is tied to `&self`), and
12484 // calling the accessor twice on the same [`Caixa`] must yield
12485 // slices that are pointer-equal (the underlying byte-buffer is
12486 // the storage `Vec`'s allocation, not a fresh copy) as well as
12487 // value-equal (idempotent, no side effects on `&self`).
12488 //
12489 // Pins against a future silent detour that returned an owned
12490 // `Vec<String>` (which would type-check but silently clone on
12491 // every call, breaking the zero-cost projection every peer
12492 // sibling slice accessor carries), a `&Vec<String>` return
12493 // (which would leak the backing `Vec`'s grow/push/reserve
12494 // surface no downstream consumer reaches for), or a one-arm-
12495 // only accessor that returned a saturating value on some
12496 // sentinel input (breaking the pass-through invariant the
12497 // sibling slice accessors carry).
12498 for autores in [
12499 vec![],
12500 vec!["pleme-io"],
12501 vec!["alice", "bob"],
12502 vec!["pleme-io", "pleme-io"],
12503 ] {
12504 let c = caixa_with_autores(autores.clone());
12505 let expected: Vec<String> = autores.iter().map(|s| (*s).to_string()).collect();
12506 let first = c.autores();
12507 let second = c.autores();
12508 assert_eq!(
12509 first, second,
12510 "Caixa::autores must be idempotent — two successive \
12511 calls on the same &self must return the same \
12512 &[String]",
12513 );
12514 assert_eq!(
12515 first.as_ptr(),
12516 second.as_ptr(),
12517 "Caixa::autores must borrow the underlying Vec<String> \
12518 storage — two successive calls must return slices \
12519 with the same backing pointer (a fresh Vec<String> \
12520 clone would change the pointer on every call)",
12521 );
12522 assert_eq!(
12523 first,
12524 expected.as_slice(),
12525 "Caixa::autores must return :autores verbatim by \
12526 borrow — got {first:?}, expected {expected:?}",
12527 );
12528 }
12529 }
12530
12531 // ── Caixa::etiquetas — outer top-level &[T] slice accessor ────────
12532
12533 #[test]
12534 fn etiquetas_returns_etiquetas_slice_verbatim_across_permutations() {
12535 // The canonical per-`Caixa` `:etiquetas` universal-axis
12536 // registry-search-tag-list slice pin: [`Caixa::etiquetas`] must
12537 // return the `:etiquetas` typed [`Vec<String>`] list verbatim
12538 // as a `&[String]`, byte-equal to the raw
12539 // `self.etiquetas.as_slice()` access across every representative
12540 // value in the accept-set — `[]` (the "no tags declared" arm
12541 // every existing fixture without an `:etiquetas` line carries),
12542 // `[""]` (a past-the-guard sentinel that pins the accessor
12543 // doesn't perform a silent `[""] → []` collapse on the empty-
12544 // entry arm — validate rejects `[""]` through `EtiquetaEmpty`
12545 // but the accessor must ship the raw slot verbatim so a
12546 // validate-time gate regression surfaces at the caixa-helm emit
12547 // boundary rather than being silently absorbed into a keyword-
12548 // drop), `["demo"]` (the canonical single-tag form every
12549 // `feira init` template scaffolds), `["example", "aplicacao",
12550 // "mesh", "ecommerce", "demo"]` (the canonical multi-tag form
12551 // the checkout-aplicacao fixture emits), and `["demo", "demo"]`
12552 // (a past-the-guard duplicate sentinel — validate rejects
12553 // through `EtiquetaDuplicate` but the accessor must ship the
12554 // raw slot verbatim so the caixa-helm `BTreeSet::collect` dedup
12555 // at chart-render time isn't silently promoted into the
12556 // accessor boundary and struct-literal
12557 // `Caixa { etiquetas: vec!["demo".into(), "demo".into()], .. }`
12558 // fixtures continue to expose the duplicate at the accessor).
12559 //
12560 // Second outer top-level [`Caixa`] `&[T]`-return slice accessor
12561 // pin on the substrate primitive — folds on the "outer
12562 // [`Caixa`] `&[T]` slice" projection pattern
12563 // `autores_returns_autores_slice_verbatim_across_permutations`
12564 // (b5d813f) opened, sibling in shape and idiom. Pins against a
12565 // future silent detour that returned an owned `Vec<String>`
12566 // (which would type-check but silently clone on every accessor
12567 // call, breaking the zero-cost projection every peer sibling
12568 // slice accessor carries), a `[""] → []` collapse (which would
12569 // silently absorb the `EtiquetaEmpty` refusal case at the
12570 // accessor boundary), or a `["a", "a"] → ["a"]` dedup collapse
12571 // (which would silently absorb the `EtiquetaDuplicate` refusal
12572 // case at the accessor boundary — the caixa-helm chart-render
12573 // `BTreeSet::collect` dedup is downstream of the accessor and
12574 // must not be silently promoted into it).
12575 for etiquetas in [
12576 vec![],
12577 vec![""],
12578 vec!["demo"],
12579 vec!["example", "aplicacao", "mesh", "ecommerce", "demo"],
12580 vec!["demo", "demo"],
12581 ] {
12582 let c = caixa_with_etiquetas(etiquetas.clone());
12583 let expected: Vec<String> = etiquetas.iter().map(|s| (*s).to_string()).collect();
12584 assert_eq!(
12585 c.etiquetas(),
12586 expected.as_slice(),
12587 "Caixa::etiquetas must return :etiquetas verbatim (got \
12588 {:?}, expected {expected:?})",
12589 c.etiquetas(),
12590 );
12591 assert_eq!(
12592 c.etiquetas(),
12593 c.etiquetas.as_slice(),
12594 "Caixa::etiquetas must byte-equal the raw \
12595 `self.etiquetas.as_slice()` field access across every \
12596 value in the Vec<String> accept-set",
12597 );
12598 }
12599 }
12600
12601 #[test]
12602 fn validate_etiquetas_empty_entry_arm_routes_through_accessor() {
12603 // Composition pin: [`Caixa::validate_etiquetas`]'s per-entry
12604 // empty-arm gate must key off [`Caixa::etiquetas`], not the raw
12605 // `&self.etiquetas` field-borrow walk. Structurally: a
12606 // `Caixa { etiquetas: vec!["".into()], .. }` must surface the
12607 // `EtiquetaEmpty` refusal exactly, and a
12608 // `Caixa { etiquetas: vec!["demo".into()], .. }` (the canonical
12609 // single-tag form) must pass validate. The pair jointly pins
12610 // the accessor + validate-gate composition: any future silent
12611 // detour that had the accessor return an empty slice on the
12612 // `[""]` arm (a
12613 // `.iter().filter(|s| !s.is_empty()).collect()` collapse) would
12614 // silently absorb the `EtiquetaEmpty` refusal at the accessor
12615 // boundary and the validate gate would accept a struct-literal
12616 // `Caixa { etiquetas: vec!["".into()], .. }` — the composition
12617 // pin catches that at caixa-core build time.
12618 //
12619 // Peer of the per-`Caixa` `validate_autores_empty_arm_routes_
12620 // through_accessor` (b5d813f) accessor-composition pin on the
12621 // sibling `&[T]`-composition axis — same "the validate / shape-
12622 // gate predicate must route through the substrate-primitive
12623 // typed dispatch" discipline extended onto the sibling outer
12624 // top-level [`Caixa`] `&[T]`-composition surface.
12625 let c = caixa_with_etiquetas(vec![""]);
12626 assert!(
12627 matches!(c.validate_etiquetas(), Err(ManifestError::EtiquetaEmpty)),
12628 "validate_etiquetas must reject etiquetas == vec![\"\"] \
12629 with EtiquetaEmpty — the accessor and the validate gate \
12630 must route through the same substrate-primitive typed \
12631 dispatch on the :etiquetas per-entry empty arm",
12632 );
12633 let c = caixa_with_etiquetas(vec!["demo"]);
12634 assert!(
12635 c.validate_etiquetas().is_ok(),
12636 "validate_etiquetas must accept etiquetas == vec![\"demo\"] \
12637 (the canonical single-tag shape every `feira init` \
12638 template scaffolds)",
12639 );
12640 }
12641
12642 #[test]
12643 fn etiquetas_projects_slice_by_borrow() {
12644 // The by-borrow pin: [`Caixa::etiquetas`] returns `&[String]`
12645 // by borrow — the returned slice borrows the underlying
12646 // `Vec<String>` storage of the `:etiquetas` slot and the
12647 // accessor must not clone the backing `Vec` on every call.
12648 // Peer of the per-`Caixa` `autores_projects_slice_by_borrow`
12649 // (b5d813f) by-borrow pin on the sibling outer top-level
12650 // [`Caixa`] `&[String]`-return axis — the accessor's returned
12651 // slice must borrow from `&self` (the returned reference's
12652 // lifetime is tied to `&self`), and calling the accessor twice
12653 // on the same [`Caixa`] must yield slices that are pointer-
12654 // equal (the underlying byte-buffer is the storage `Vec`'s
12655 // allocation, not a fresh copy) as well as value-equal
12656 // (idempotent, no side effects on `&self`).
12657 //
12658 // Pins against a future silent detour that returned an owned
12659 // `Vec<String>` (which would type-check but silently clone on
12660 // every call, breaking the zero-cost projection every peer
12661 // sibling slice accessor carries), a `&Vec<String>` return
12662 // (which would leak the backing `Vec`'s grow/push/reserve
12663 // surface no downstream consumer reaches for), or a one-arm-
12664 // only accessor that returned a saturating value on some
12665 // sentinel input (breaking the pass-through invariant the
12666 // sibling slice accessors carry).
12667 for etiquetas in [
12668 vec![],
12669 vec!["demo"],
12670 vec!["example", "aplicacao", "mesh"],
12671 vec!["demo", "demo"],
12672 ] {
12673 let c = caixa_with_etiquetas(etiquetas.clone());
12674 let expected: Vec<String> = etiquetas.iter().map(|s| (*s).to_string()).collect();
12675 let first = c.etiquetas();
12676 let second = c.etiquetas();
12677 assert_eq!(
12678 first, second,
12679 "Caixa::etiquetas must be idempotent — two successive \
12680 calls on the same &self must return the same \
12681 &[String]",
12682 );
12683 assert_eq!(
12684 first.as_ptr(),
12685 second.as_ptr(),
12686 "Caixa::etiquetas must borrow the underlying \
12687 Vec<String> storage — two successive calls must \
12688 return slices with the same backing pointer (a fresh \
12689 Vec<String> clone would change the pointer on every \
12690 call)",
12691 );
12692 assert_eq!(
12693 first,
12694 expected.as_slice(),
12695 "Caixa::etiquetas must return :etiquetas verbatim by \
12696 borrow — got {first:?}, expected {expected:?}",
12697 );
12698 }
12699 }
12700
12701 // ── Caixa::bibliotecas — outer top-level &[T] slice accessor ──────
12702
12703 #[test]
12704 fn bibliotecas_returns_bibliotecas_slice_verbatim_across_permutations() {
12705 // The canonical per-`Caixa` `:bibliotecas` universal-axis
12706 // library-source-path-list slice pin: [`Caixa::bibliotecas`]
12707 // must return the `:bibliotecas` typed [`Vec<String>`] list
12708 // verbatim as a `&[String]`, byte-equal to the raw
12709 // `self.bibliotecas.as_slice()` access across every
12710 // representative value in the accept-set — `[]` (the "no
12711 // libraries declared" arm every `:kind` other than `Biblioteca`
12712 // + every `Biblioteca` relying on the canonical
12713 // `lib/<nome>.lisp` implicit-default path carries; the
12714 // layout's [`crate::LayoutInvariants`] `MissingLib` arm-gate
12715 // fires exactly on this empty-slot + `Biblioteca`-kind
12716 // combination), `[""]` (a past-the-guard sentinel that pins
12717 // the accessor doesn't perform a silent `[""] → []` collapse
12718 // on the empty-entry arm — validate rejects `[""]` through
12719 // `CodePathEmpty { slot: ":bibliotecas" }` but the accessor
12720 // must ship the raw slot verbatim so a validate-time gate
12721 // regression surfaces at the `feira build` phase-1 parse
12722 // boundary rather than being silently absorbed into a
12723 // library-drop), `["lib/demo.lisp"]` (the canonical single-
12724 // entry form `Caixa::template` scaffolds and every `feira init`
12725 // template emits), `["lib/demo.lisp", "lib/helpers.lisp"]`
12726 // (the canonical multi-library form the
12727 // `validate_code_paths_accepts_explicit_relative_paths_on_
12728 // every_slot` fixture emits), and `["lib/foo.lisp",
12729 // "lib/foo.lisp"]` (a past-the-guard duplicate sentinel —
12730 // validate rejects through `CodePathDuplicate { slot:
12731 // ":bibliotecas" }` per the per-slot set-not-multiset gate,
12732 // but the accessor must ship the raw slot verbatim so the
12733 // `feira build` `for entry in caixa.bibliotecas()` parse walk
12734 // sees the duplicate at the accessor boundary and struct-
12735 // literal `Caixa { bibliotecas: vec!["lib/foo.lisp".into(),
12736 // "lib/foo.lisp".into()], .. }` fixtures continue to expose
12737 // the duplicate at the accessor).
12738 //
12739 // Third outer top-level [`Caixa`] `&[T]`-return slice accessor
12740 // pin on the substrate primitive — folds on the "outer
12741 // [`Caixa`] `&[T]` slice" projection pattern
12742 // `autores_returns_autores_slice_verbatim_across_permutations`
12743 // (b5d813f) opened and
12744 // `etiquetas_returns_etiquetas_slice_verbatim_across_permutations`
12745 // (78c7d3c) folded on, sibling in shape and idiom. Pins
12746 // against a future silent detour that returned an owned
12747 // `Vec<String>` (which would type-check but silently clone on
12748 // every accessor call, breaking the zero-cost projection
12749 // every peer sibling slice accessor carries), a `[""] → []`
12750 // collapse (which would silently absorb the `CodePathEmpty`
12751 // refusal case at the accessor boundary), or a `["lib/foo.lisp",
12752 // "lib/foo.lisp"] → ["lib/foo.lisp"]` dedup collapse (which
12753 // would silently absorb the `CodePathDuplicate` refusal case
12754 // at the accessor boundary — the per-slot set-not-multiset
12755 // gate is downstream of the accessor and must not be silently
12756 // promoted into it).
12757 for bibliotecas in [
12758 vec![],
12759 vec![""],
12760 vec!["lib/demo.lisp"],
12761 vec!["lib/demo.lisp", "lib/helpers.lisp"],
12762 vec!["lib/foo.lisp", "lib/foo.lisp"],
12763 ] {
12764 let c = caixa_with_code_paths(bibliotecas.clone(), vec![], vec![]);
12765 let expected: Vec<String> = bibliotecas.iter().map(|s| (*s).to_string()).collect();
12766 assert_eq!(
12767 c.bibliotecas(),
12768 expected.as_slice(),
12769 "Caixa::bibliotecas must return :bibliotecas verbatim \
12770 (got {:?}, expected {expected:?})",
12771 c.bibliotecas(),
12772 );
12773 assert_eq!(
12774 c.bibliotecas(),
12775 c.bibliotecas.as_slice(),
12776 "Caixa::bibliotecas must byte-equal the raw \
12777 `self.bibliotecas.as_slice()` field access across \
12778 every value in the Vec<String> accept-set",
12779 );
12780 }
12781 }
12782
12783 #[test]
12784 fn validate_code_paths_bibliotecas_empty_arm_routes_through_accessor() {
12785 // Composition pin: [`Caixa::validate_code_paths`]'s per-entry
12786 // empty-arm gate on the `:bibliotecas` slot must key off
12787 // [`Caixa::bibliotecas`], not a divergent raw
12788 // `&self.bibliotecas` field-borrow walk. Structurally: a
12789 // `Caixa { bibliotecas: vec!["".into()], .. }` must surface
12790 // the `CodePathEmpty { slot: ":bibliotecas" }` refusal
12791 // exactly, and a `Caixa { bibliotecas: vec!["lib/demo.lisp".
12792 // into()], .. }` (the canonical single-library form
12793 // `Caixa::template` scaffolds) must pass validate. The pair
12794 // jointly pins the accessor + validate-gate composition: any
12795 // future silent detour that had the accessor return an empty
12796 // slice on the `[""]` arm (a `.iter().filter(|s|
12797 // !s.is_empty()).collect()` collapse) would silently absorb
12798 // the `CodePathEmpty` refusal at the accessor boundary and
12799 // the validate gate would accept a struct-literal
12800 // `Caixa { bibliotecas: vec!["".into()], .. }` — the
12801 // composition pin catches that at caixa-core build time.
12802 //
12803 // Peer of the per-`Caixa` `validate_autores_empty_arm_routes_
12804 // through_accessor` (b5d813f) and
12805 // `validate_etiquetas_empty_entry_arm_routes_through_accessor`
12806 // (78c7d3c) accessor-composition pins on the sibling `&[T]`-
12807 // composition axes — same "the validate / shape-gate
12808 // predicate must route through the substrate-primitive typed
12809 // dispatch" discipline extended onto the sibling outer top-
12810 // level [`Caixa`] `&[T]`-composition surface. Nominally the
12811 // in-tree `validate_code_paths` production body still keys
12812 // off the internal `[(":bibliotecas", &self.bibliotecas,
12813 // CodePathFileType::LispSource), (":exe", &self.exe, ..),
12814 // (":servicos", &self.servicos, ..)]` per-slot dispatch tuple
12815 // (the tuple's homogeneous slice-typed shape blocks a per-
12816 // element accessor swap in isolation — a future companion
12817 // lift for `:exe` and `:servicos` on the same outer-`Caixa`
12818 // `&[T]` slice-accessor axis closes that tuple onto the
12819 // triple of typed dispatches as a unit); the composition pin
12820 // catches any future accessor-side silent filter drop against
12821 // that eventual tuple-closure regardless of whether the
12822 // `:bibliotecas` slot is threaded through the accessor or the
12823 // raw field access at the tuple's construction site.
12824 let c = caixa_with_code_paths(vec![""], vec![], vec![]);
12825 assert!(
12826 matches!(
12827 c.validate_code_paths(),
12828 Err(ManifestError::CodePathEmpty {
12829 slot: ":bibliotecas"
12830 })
12831 ),
12832 "validate_code_paths must reject bibliotecas == vec![\"\"] \
12833 with CodePathEmpty {{ slot: \":bibliotecas\" }} — the \
12834 accessor and the validate gate must route through the \
12835 same substrate-primitive typed dispatch on the \
12836 :bibliotecas per-entry empty arm",
12837 );
12838 let c = caixa_with_code_paths(vec!["lib/demo.lisp"], vec![], vec![]);
12839 assert!(
12840 c.validate_code_paths().is_ok(),
12841 "validate_code_paths must accept bibliotecas == \
12842 vec![\"lib/demo.lisp\"] (the canonical single-library \
12843 shape every `feira init` template scaffolds)",
12844 );
12845 }
12846
12847 #[test]
12848 fn bibliotecas_projects_slice_by_borrow() {
12849 // The by-borrow pin: [`Caixa::bibliotecas`] returns
12850 // `&[String]` by borrow — the returned slice borrows the
12851 // underlying `Vec<String>` storage of the `:bibliotecas` slot
12852 // and the accessor must not clone the backing `Vec` on every
12853 // call. Peer of the per-`Caixa` `autores_projects_slice_by_borrow`
12854 // (b5d813f) and `etiquetas_projects_slice_by_borrow` (78c7d3c)
12855 // by-borrow pins on the sibling outer top-level [`Caixa`]
12856 // `&[String]`-return axes — the accessor's returned slice
12857 // must borrow from `&self` (the returned reference's lifetime
12858 // is tied to `&self`), and calling the accessor twice on the
12859 // same [`Caixa`] must yield slices that are pointer-equal
12860 // (the underlying byte-buffer is the storage `Vec`'s
12861 // allocation, not a fresh copy) as well as value-equal
12862 // (idempotent, no side effects on `&self`).
12863 //
12864 // Pins against a future silent detour that returned an owned
12865 // `Vec<String>` (which would type-check but silently clone on
12866 // every call, breaking the zero-cost projection every peer
12867 // sibling slice accessor carries), a `&Vec<String>` return
12868 // (which would leak the backing `Vec`'s grow/push/reserve
12869 // surface no downstream consumer reaches for), or a one-arm-
12870 // only accessor that returned a saturating value on some
12871 // sentinel input (breaking the pass-through invariant the
12872 // sibling slice accessors carry).
12873 for bibliotecas in [
12874 vec![],
12875 vec!["lib/demo.lisp"],
12876 vec!["lib/demo.lisp", "lib/helpers.lisp"],
12877 vec!["lib/foo.lisp", "lib/foo.lisp"],
12878 ] {
12879 let c = caixa_with_code_paths(bibliotecas.clone(), vec![], vec![]);
12880 let expected: Vec<String> = bibliotecas.iter().map(|s| (*s).to_string()).collect();
12881 let first = c.bibliotecas();
12882 let second = c.bibliotecas();
12883 assert_eq!(
12884 first, second,
12885 "Caixa::bibliotecas must be idempotent — two \
12886 successive calls on the same &self must return the \
12887 same &[String]",
12888 );
12889 assert_eq!(
12890 first.as_ptr(),
12891 second.as_ptr(),
12892 "Caixa::bibliotecas must borrow the underlying \
12893 Vec<String> storage — two successive calls must \
12894 return slices with the same backing pointer (a \
12895 fresh Vec<String> clone would change the pointer on \
12896 every call)",
12897 );
12898 assert_eq!(
12899 first,
12900 expected.as_slice(),
12901 "Caixa::bibliotecas must return :bibliotecas verbatim \
12902 by borrow — got {first:?}, expected {expected:?}",
12903 );
12904 }
12905 }
12906
12907 // ── Caixa::exe — outer top-level &[T] slice accessor ──────────────
12908
12909 #[test]
12910 fn exe_returns_exe_slice_verbatim_across_permutations() {
12911 // The canonical per-`Caixa` `:exe` universal-axis
12912 // nix-built-executable-entry-path-list slice pin: [`Caixa::exe`]
12913 // must return the `:exe` typed [`Vec<String>`] list verbatim as
12914 // a `&[String]`, byte-equal to the raw `self.exe.as_slice()`
12915 // access across every representative value in the accept-set —
12916 // `[]` (the "no executable declared" arm every `:kind` other
12917 // than `Binario` carries; the layout's [`crate::LayoutInvariants`]
12918 // `BinarioWithoutExe` arm-gate fires exactly on this empty-slot
12919 // + `Binario`-kind combination), `[""]` (a past-the-guard
12920 // sentinel that pins the accessor doesn't perform a silent
12921 // `[""] → []` collapse on the empty-entry arm — validate rejects
12922 // `[""]` through `CodePathEmpty { slot: ":exe" }` but the
12923 // accessor must ship the raw slot verbatim so a validate-time
12924 // gate regression surfaces at the layout / `feira nix` boundary
12925 // rather than being silently absorbed into an executable-drop),
12926 // `["exe/cli"]` (the canonical single-entry Binario form every
12927 // in-tree `caixa_with_code_paths` positive control uses),
12928 // `["exe/cli", "exe/serve"]` (the canonical multi-executable
12929 // form the `validate_code_paths_accepts_explicit_relative_paths_
12930 // on_every_slot` fixture emits), and `["exe/cli", "exe/cli"]`
12931 // (a past-the-guard duplicate sentinel — validate rejects
12932 // through `CodePathDuplicate { slot: ":exe" }` per the per-slot
12933 // set-not-multiset gate, but the accessor must ship the raw
12934 // slot verbatim so struct-literal `Caixa { exe: vec!["exe/cli".
12935 // into(), "exe/cli".into()], .. }` fixtures continue to expose
12936 // the duplicate at the accessor).
12937 //
12938 // Fourth outer top-level [`Caixa`] `&[T]`-return slice accessor
12939 // pin on the substrate primitive — folds on the "outer
12940 // [`Caixa`] `&[T]` slice" projection pattern
12941 // `autores_returns_autores_slice_verbatim_across_permutations`
12942 // (b5d813f) opened,
12943 // `etiquetas_returns_etiquetas_slice_verbatim_across_permutations`
12944 // (78c7d3c) folded on, and
12945 // `bibliotecas_returns_bibliotecas_slice_verbatim_across_permutations`
12946 // (8a36c23) closed the universal-axis text-tag family of.
12947 // Opens the outer-`Caixa` foreign-code-slot `&[T]` sub-family
12948 // the sibling `:servicos` future lift closes onto. Pins against
12949 // a future silent detour that returned an owned `Vec<String>`
12950 // (which would type-check but silently clone on every accessor
12951 // call, breaking the zero-cost projection every peer sibling
12952 // slice accessor carries), a `[""] → []` collapse (which would
12953 // silently absorb the `CodePathEmpty` refusal case at the
12954 // accessor boundary), or an `["exe/cli", "exe/cli"] →
12955 // ["exe/cli"]` dedup collapse (which would silently absorb the
12956 // `CodePathDuplicate` refusal case at the accessor boundary —
12957 // the per-slot set-not-multiset gate is downstream of the
12958 // accessor and must not be silently promoted into it).
12959 for exe in [
12960 vec![],
12961 vec![""],
12962 vec!["exe/cli"],
12963 vec!["exe/cli", "exe/serve"],
12964 vec!["exe/cli", "exe/cli"],
12965 ] {
12966 let c = caixa_with_code_paths(vec![], exe.clone(), vec![]);
12967 let expected: Vec<String> = exe.iter().map(|s| (*s).to_string()).collect();
12968 assert_eq!(
12969 c.exe(),
12970 expected.as_slice(),
12971 "Caixa::exe must return :exe verbatim (got {:?}, \
12972 expected {expected:?})",
12973 c.exe(),
12974 );
12975 assert_eq!(
12976 c.exe(),
12977 c.exe.as_slice(),
12978 "Caixa::exe must byte-equal the raw \
12979 `self.exe.as_slice()` field access across every value \
12980 in the Vec<String> accept-set",
12981 );
12982 }
12983 }
12984
12985 #[test]
12986 fn validate_code_paths_exe_empty_arm_routes_through_accessor() {
12987 // Composition pin: [`Caixa::validate_code_paths`]'s per-entry
12988 // empty-arm gate on the `:exe` slot must key off
12989 // [`Caixa::exe`], not a divergent raw `&self.exe` field-borrow
12990 // walk. Structurally: a `Caixa { exe: vec!["".into()], .. }`
12991 // must surface the `CodePathEmpty { slot: ":exe" }` refusal
12992 // exactly, and a `Caixa { exe: vec!["exe/cli".into()], .. }`
12993 // (the canonical single-executable form every in-tree
12994 // `caixa_with_code_paths` positive control uses) must pass
12995 // validate. The pair jointly pins the accessor + validate-gate
12996 // composition: any future silent detour that had the accessor
12997 // return an empty slice on the `[""]` arm (a
12998 // `.iter().filter(|s| !s.is_empty()).collect()` collapse) would
12999 // silently absorb the `CodePathEmpty` refusal at the accessor
13000 // boundary and the validate gate would accept a struct-literal
13001 // `Caixa { exe: vec!["".into()], .. }` — the composition pin
13002 // catches that at caixa-core build time.
13003 //
13004 // Peer of the per-`Caixa`
13005 // `validate_code_paths_bibliotecas_empty_arm_routes_through_accessor`
13006 // (8a36c23), `validate_autores_empty_arm_routes_through_accessor`
13007 // (b5d813f), and
13008 // `validate_etiquetas_empty_entry_arm_routes_through_accessor`
13009 // (78c7d3c) accessor-composition pins on the sibling `&[T]`-
13010 // composition axes — same "the validate / shape-gate predicate
13011 // must route through the substrate-primitive typed dispatch"
13012 // discipline extended onto the sibling outer top-level [`Caixa`]
13013 // `&[T]`-composition surface. Nominally the in-tree
13014 // `validate_code_paths` production body still keys off the
13015 // internal `[(":bibliotecas", &self.bibliotecas,
13016 // CodePathFileType::LispSource), (":exe", &self.exe, ..),
13017 // (":servicos", &self.servicos, ..)]` per-slot dispatch tuple
13018 // (the tuple's homogeneous slice-typed shape blocks a per-
13019 // element accessor swap in isolation — a future companion lift
13020 // for `:servicos` on the same outer-`Caixa` `&[T]` slice-
13021 // accessor axis closes that tuple onto the triple of typed
13022 // dispatches as a unit); the composition pin catches any future
13023 // accessor-side silent filter drop against that eventual tuple-
13024 // closure regardless of whether the `:exe` slot is threaded
13025 // through the accessor or the raw field access at the tuple's
13026 // construction site.
13027 let c = caixa_with_code_paths(vec![], vec![""], vec![]);
13028 assert!(
13029 matches!(
13030 c.validate_code_paths(),
13031 Err(ManifestError::CodePathEmpty { slot: ":exe" })
13032 ),
13033 "validate_code_paths must reject exe == vec![\"\"] \
13034 with CodePathEmpty {{ slot: \":exe\" }} — the \
13035 accessor and the validate gate must route through the \
13036 same substrate-primitive typed dispatch on the \
13037 :exe per-entry empty arm",
13038 );
13039 let c = caixa_with_code_paths(vec![], vec!["exe/cli"], vec![]);
13040 assert!(
13041 c.validate_code_paths().is_ok(),
13042 "validate_code_paths must accept exe == vec![\"exe/cli\"] \
13043 (the canonical single-executable shape every in-tree \
13044 `caixa_with_code_paths` positive control uses)",
13045 );
13046 }
13047
13048 #[test]
13049 fn exe_projects_slice_by_borrow() {
13050 // The by-borrow pin: [`Caixa::exe`] returns `&[String]` by
13051 // borrow — the returned slice borrows the underlying
13052 // `Vec<String>` storage of the `:exe` slot and the accessor
13053 // must not clone the backing `Vec` on every call. Peer of the
13054 // per-`Caixa` `autores_projects_slice_by_borrow` (b5d813f),
13055 // `etiquetas_projects_slice_by_borrow` (78c7d3c), and
13056 // `bibliotecas_projects_slice_by_borrow` (8a36c23) by-borrow
13057 // pins on the sibling outer top-level [`Caixa`] `&[String]`-
13058 // return axes — the accessor's returned slice must borrow from
13059 // `&self` (the returned reference's lifetime is tied to
13060 // `&self`), and calling the accessor twice on the same
13061 // [`Caixa`] must yield slices that are pointer-equal (the
13062 // underlying byte-buffer is the storage `Vec`'s allocation,
13063 // not a fresh copy) as well as value-equal (idempotent, no
13064 // side effects on `&self`).
13065 //
13066 // Pins against a future silent detour that returned an owned
13067 // `Vec<String>` (which would type-check but silently clone on
13068 // every call, breaking the zero-cost projection every peer
13069 // sibling slice accessor carries), a `&Vec<String>` return
13070 // (which would leak the backing `Vec`'s grow/push/reserve
13071 // surface no downstream consumer reaches for), or a one-arm-
13072 // only accessor that returned a saturating value on some
13073 // sentinel input (breaking the pass-through invariant the
13074 // sibling slice accessors carry).
13075 for exe in [
13076 vec![],
13077 vec!["exe/cli"],
13078 vec!["exe/cli", "exe/serve"],
13079 vec!["exe/cli", "exe/cli"],
13080 ] {
13081 let c = caixa_with_code_paths(vec![], exe.clone(), vec![]);
13082 let expected: Vec<String> = exe.iter().map(|s| (*s).to_string()).collect();
13083 let first = c.exe();
13084 let second = c.exe();
13085 assert_eq!(
13086 first, second,
13087 "Caixa::exe must be idempotent — two successive calls \
13088 on the same &self must return the same &[String]",
13089 );
13090 assert_eq!(
13091 first.as_ptr(),
13092 second.as_ptr(),
13093 "Caixa::exe must borrow the underlying Vec<String> \
13094 storage — two successive calls must return slices \
13095 with the same backing pointer (a fresh Vec<String> \
13096 clone would change the pointer on every call)",
13097 );
13098 assert_eq!(
13099 first,
13100 expected.as_slice(),
13101 "Caixa::exe must return :exe verbatim by borrow — \
13102 got {first:?}, expected {expected:?}",
13103 );
13104 }
13105 }
13106
13107 // ── Caixa::servicos — outer top-level &[T] slice accessor ─────────
13108
13109 #[test]
13110 fn servicos_returns_servicos_slice_verbatim_across_permutations() {
13111 // The canonical per-`Caixa` `:servicos` universal-axis
13112 // ComputeUnit-CR-YAML-entry-path-list slice pin:
13113 // [`Caixa::servicos`] must return the `:servicos` typed
13114 // [`Vec<String>`] list verbatim as a `&[String]`, byte-equal to
13115 // the raw `self.servicos.as_slice()` access across every
13116 // representative value in the accept-set — `[]` (the "no
13117 // ComputeUnit-CR declared" arm every `:kind` other than
13118 // `Servico` carries; the layout's [`crate::LayoutInvariants`]
13119 // `ServicoWithoutServicos` arm-gate fires exactly on this
13120 // empty-slot + `Servico`-kind combination), `[""]` (a past-the-
13121 // guard sentinel that pins the accessor doesn't perform a
13122 // silent `[""] → []` collapse on the empty-entry arm — validate
13123 // rejects `[""]` through `CodePathEmpty { slot: ":servicos" }`
13124 // but the accessor must ship the raw slot verbatim so a
13125 // validate-time gate regression surfaces at the layout /
13126 // per-Servico renderer boundary rather than being silently
13127 // absorbed into a component-drop),
13128 // `["servicos/demo.computeunit.yaml"]` (the canonical
13129 // singleton V0-shape every in-tree `caixa_with_code_paths`
13130 // positive control uses; the same shape
13131 // [`crate::require_single_servico`] admits),
13132 // `["servicos/a.computeunit.yaml", "servicos/b.computeunit.
13133 // yaml"]` (a past-the-guard `len != 1` sentinel — the V0
13134 // singularity gate rejects through `ServicoCountMismatch
13135 // { count: 2 }` but the accessor must ship the raw slot
13136 // verbatim so struct-literal `Caixa { servicos: vec![...,
13137 // ...], .. }` fixtures continue to expose the count at the
13138 // accessor), and `["servicos/a.computeunit.yaml",
13139 // "servicos/a.computeunit.yaml"]` (a past-the-guard duplicate
13140 // sentinel — validate rejects through
13141 // `CodePathDuplicate { slot: ":servicos" }` per the per-slot
13142 // set-not-multiset gate, but the accessor must ship the raw
13143 // slot verbatim so struct-literal fixtures continue to expose
13144 // the duplicate at the accessor).
13145 //
13146 // Fifth and final outer top-level [`Caixa`] `&[T]`-return
13147 // slice accessor pin on the substrate primitive — folds on the
13148 // "outer [`Caixa`] `&[T]` slice" projection pattern
13149 // `autores_returns_autores_slice_verbatim_across_permutations`
13150 // (b5d813f) opened,
13151 // `etiquetas_returns_etiquetas_slice_verbatim_across_permutations`
13152 // (78c7d3c) folded on,
13153 // `bibliotecas_returns_bibliotecas_slice_verbatim_across_permutations`
13154 // (8a36c23) closed the universal-axis text-tag family of, and
13155 // `exe_returns_exe_slice_verbatim_across_permutations`
13156 // (65d9527) opened the foreign-code-slot sub-family of. Closes
13157 // the outer-`Caixa` foreign-code-slot `&[T]` sub-family — the
13158 // trio of code-surface list slots (`:bibliotecas` + `:exe` +
13159 // `:servicos`) now each carries a substrate-canonical slice
13160 // accessor. Pins against a future silent detour that returned
13161 // an owned `Vec<String>` (which would type-check but silently
13162 // clone on every accessor call, breaking the zero-cost
13163 // projection every peer sibling slice accessor carries), a
13164 // `[""] → []` collapse (which would silently absorb the
13165 // `CodePathEmpty` refusal case at the accessor boundary), an
13166 // `[a, a] → [a]` dedup collapse (which would silently absorb
13167 // the `CodePathDuplicate` refusal case at the accessor
13168 // boundary — the per-slot set-not-multiset gate is downstream
13169 // of the accessor and must not be silently promoted into it),
13170 // or a `[a, b] → [a]` singleton collapse (which would silently
13171 // absorb the V0 `ServicoCountMismatch` refusal case at the
13172 // accessor boundary — the V0 singularity gate is downstream of
13173 // the accessor and must not be silently promoted into it).
13174 for servicos in [
13175 vec![],
13176 vec![""],
13177 vec!["servicos/demo.computeunit.yaml"],
13178 vec!["servicos/a.computeunit.yaml", "servicos/b.computeunit.yaml"],
13179 vec!["servicos/a.computeunit.yaml", "servicos/a.computeunit.yaml"],
13180 ] {
13181 let c = caixa_with_code_paths(vec![], vec![], servicos.clone());
13182 let expected: Vec<String> = servicos.iter().map(|s| (*s).to_string()).collect();
13183 assert_eq!(
13184 c.servicos(),
13185 expected.as_slice(),
13186 "Caixa::servicos must return :servicos verbatim (got \
13187 {:?}, expected {expected:?})",
13188 c.servicos(),
13189 );
13190 assert_eq!(
13191 c.servicos(),
13192 c.servicos.as_slice(),
13193 "Caixa::servicos must byte-equal the raw \
13194 `self.servicos.as_slice()` field access across every \
13195 value in the Vec<String> accept-set",
13196 );
13197 }
13198 }
13199
13200 #[test]
13201 fn validate_code_paths_servicos_empty_arm_routes_through_accessor() {
13202 // Composition pin: [`Caixa::validate_code_paths`]'s per-entry
13203 // empty-arm gate on the `:servicos` slot must key off
13204 // [`Caixa::servicos`], not a divergent raw `&self.servicos`
13205 // field-borrow walk. Structurally: a `Caixa { servicos:
13206 // vec!["".into()], .. }` must surface the `CodePathEmpty
13207 // { slot: ":servicos" }` refusal exactly, and a `Caixa
13208 // { servicos: vec!["servicos/demo.computeunit.yaml".into()],
13209 // .. }` (the canonical singleton V0-shape every in-tree
13210 // `caixa_with_code_paths` positive control uses) must pass
13211 // validate. The pair jointly pins the accessor + validate-gate
13212 // composition: any future silent detour that had the accessor
13213 // return an empty slice on the `[""]` arm (a `.iter().filter
13214 // (|s| !s.is_empty()).collect()` collapse) would silently
13215 // absorb the `CodePathEmpty` refusal at the accessor boundary
13216 // and the validate gate would accept a struct-literal
13217 // `Caixa { servicos: vec!["".into()], .. }` — the composition
13218 // pin catches that at caixa-core build time.
13219 //
13220 // Peer of the per-`Caixa`
13221 // `validate_code_paths_bibliotecas_empty_arm_routes_through_accessor`
13222 // (8a36c23), `validate_code_paths_exe_empty_arm_routes_through_accessor`
13223 // (65d9527), `validate_autores_empty_arm_routes_through_accessor`
13224 // (b5d813f), and
13225 // `validate_etiquetas_empty_entry_arm_routes_through_accessor`
13226 // (78c7d3c) accessor-composition pins on the sibling `&[T]`-
13227 // composition axes — same "the validate / shape-gate predicate
13228 // must route through the substrate-primitive typed dispatch"
13229 // discipline extended onto the sibling outer top-level
13230 // [`Caixa`] `&[T]`-composition surface, closing the trio of
13231 // code-surface accessor-composition pins on the same axis.
13232 // Nominally the in-tree `validate_code_paths` production body
13233 // still keys off the internal
13234 // `[(":bibliotecas", &self.bibliotecas,
13235 // CodePathFileType::LispSource), (":exe", &self.exe, ..),
13236 // (":servicos", &self.servicos, ..)]` per-slot dispatch tuple
13237 // (the tuple's homogeneous `&Vec<String>`-typed shape blocks a
13238 // per-element accessor swap in isolation — a future companion
13239 // lift promotes the tuple's element type to `&[String]` and
13240 // threads the triple of typed dispatches through as a unit);
13241 // the composition pin catches any future accessor-side silent
13242 // filter drop against that eventual tuple-closure regardless
13243 // of whether the `:servicos` slot is threaded through the
13244 // accessor or the raw field access at the tuple's construction
13245 // site.
13246 let c = caixa_with_code_paths(vec![], vec![], vec![""]);
13247 assert!(
13248 matches!(
13249 c.validate_code_paths(),
13250 Err(ManifestError::CodePathEmpty { slot: ":servicos" })
13251 ),
13252 "validate_code_paths must reject servicos == vec![\"\"] \
13253 with CodePathEmpty {{ slot: \":servicos\" }} — the \
13254 accessor and the validate gate must route through the \
13255 same substrate-primitive typed dispatch on the \
13256 :servicos per-entry empty arm",
13257 );
13258 let c = caixa_with_code_paths(vec![], vec![], vec!["servicos/demo.computeunit.yaml"]);
13259 assert!(
13260 c.validate_code_paths().is_ok(),
13261 "validate_code_paths must accept servicos == \
13262 vec![\"servicos/demo.computeunit.yaml\"] (the canonical \
13263 singleton V0-shape every in-tree `caixa_with_code_paths` \
13264 positive control uses)",
13265 );
13266 }
13267
13268 #[test]
13269 fn servicos_projects_slice_by_borrow() {
13270 // The by-borrow pin: [`Caixa::servicos`] returns `&[String]` by
13271 // borrow — the returned slice borrows the underlying
13272 // `Vec<String>` storage of the `:servicos` slot and the
13273 // accessor must not clone the backing `Vec` on every call.
13274 // Peer of the per-`Caixa` `autores_projects_slice_by_borrow`
13275 // (b5d813f), `etiquetas_projects_slice_by_borrow` (78c7d3c),
13276 // `bibliotecas_projects_slice_by_borrow` (8a36c23), and
13277 // `exe_projects_slice_by_borrow` (65d9527) by-borrow pins on
13278 // the sibling outer top-level [`Caixa`] `&[String]`-return
13279 // axes — the accessor's returned slice must borrow from
13280 // `&self` (the returned reference's lifetime is tied to
13281 // `&self`), and calling the accessor twice on the same
13282 // [`Caixa`] must yield slices that are pointer-equal (the
13283 // underlying byte-buffer is the storage `Vec`'s allocation,
13284 // not a fresh copy) as well as value-equal (idempotent, no
13285 // side effects on `&self`).
13286 //
13287 // Pins against a future silent detour that returned an owned
13288 // `Vec<String>` (which would type-check but silently clone on
13289 // every call, breaking the zero-cost projection every peer
13290 // sibling slice accessor carries), a `&Vec<String>` return
13291 // (which would leak the backing `Vec`'s grow/push/reserve
13292 // surface no downstream consumer reaches for), or a one-arm-
13293 // only accessor that returned a saturating value on some
13294 // sentinel input (breaking the pass-through invariant the
13295 // sibling slice accessors carry).
13296 for servicos in [
13297 vec![],
13298 vec!["servicos/demo.computeunit.yaml"],
13299 vec!["servicos/a.computeunit.yaml", "servicos/b.computeunit.yaml"],
13300 vec!["servicos/a.computeunit.yaml", "servicos/a.computeunit.yaml"],
13301 ] {
13302 let c = caixa_with_code_paths(vec![], vec![], servicos.clone());
13303 let expected: Vec<String> = servicos.iter().map(|s| (*s).to_string()).collect();
13304 let first = c.servicos();
13305 let second = c.servicos();
13306 assert_eq!(
13307 first, second,
13308 "Caixa::servicos must be idempotent — two successive \
13309 calls on the same &self must return the same &[String]",
13310 );
13311 assert_eq!(
13312 first.as_ptr(),
13313 second.as_ptr(),
13314 "Caixa::servicos must borrow the underlying \
13315 Vec<String> storage — two successive calls must \
13316 return slices with the same backing pointer (a fresh \
13317 Vec<String> clone would change the pointer on every \
13318 call)",
13319 );
13320 assert_eq!(
13321 first,
13322 expected.as_slice(),
13323 "Caixa::servicos must return :servicos verbatim by \
13324 borrow — got {first:?}, expected {expected:?}",
13325 );
13326 }
13327 }
13328
13329 // ── Caixa::deps — outer top-level &[Dep] slice accessor ───────────
13330
13331 fn caixa_with_deps(deps: Vec<Dep>) -> Caixa {
13332 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
13333 c.deps = deps;
13334 c
13335 }
13336
13337 #[test]
13338 fn deps_returns_deps_slice_verbatim_across_permutations() {
13339 // The canonical per-`Caixa` `:deps` universal-axis runtime-
13340 // dependency-declaration-list slice pin: [`Caixa::deps`] must
13341 // return the `:deps` typed [`Vec<Dep>`] list verbatim as a
13342 // `&[Dep]`, element-equal to the raw `self.deps.as_slice()`
13343 // access across every representative value in the accept-set —
13344 // `[]` (the "no runtime deps declared" arm every existing
13345 // fixture without a `:deps` line carries; the
13346 // [`Caixa::template`] scaffold emits `:deps ()`), a canonical
13347 // single-entry list (the shape most consumer caixas carry), a
13348 // canonical two-entry list (the multi-dep runtime closure), and
13349 // two past-the-guard sentinels — a `[""]`-`:nome` entry
13350 // ([`Self::validate_deps`] rejects through `NomeEmpty` /
13351 // `NomeInvalid` but the accessor must ship the raw slot
13352 // verbatim) and a `[a, a]` duplicate (validate rejects through
13353 // `DuplicateNome { list: ":deps" }` but the accessor must ship
13354 // the raw slot verbatim so struct-literal fixtures continue to
13355 // expose the duplicate at the accessor).
13356 //
13357 // First outer top-level [`Caixa`] `&[Dep]`-return slice accessor
13358 // pin on the substrate primitive — opens the outer-`Caixa`
13359 // dependency-slot `&[Dep]` sub-family the sibling `:deps-dev`
13360 // future lift closes on. Peer of the closed outer-`Caixa`
13361 // foreign-code-slot `&[String]` sub-family
13362 // (`bibliotecas_returns_bibliotecas_slice_verbatim_across_permutations`
13363 // 8a36c23, `exe_returns_exe_slice_verbatim_across_permutations`
13364 // 65d9527, `servicos_returns_servicos_slice_verbatim_across_permutations`
13365 // 611f78b) and the outer-`Caixa` universal-axis text-tag family
13366 // (`autores_returns_autores_slice_verbatim_across_permutations`
13367 // b5d813f, `etiquetas_returns_etiquetas_slice_verbatim_across_permutations`
13368 // 78c7d3c) — extends the "outer [`Caixa`] `&[T]` slice"
13369 // projection pattern onto a novel element-type axis (`Dep`
13370 // composite vs the prior sibling family's `String` scalar).
13371 // Pins against a future silent detour that returned an owned
13372 // `Vec<Dep>` (which would type-check but silently clone on every
13373 // accessor call, breaking the zero-cost projection every peer
13374 // sibling slice accessor carries), a `[""] → []` collapse (which
13375 // would silently absorb the `NomeEmpty` refusal case at the
13376 // accessor boundary), or a `[a, a] → [a]` dedup collapse (which
13377 // would silently absorb the `DuplicateNome` refusal case at the
13378 // accessor boundary).
13379 for deps in [
13380 vec![],
13381 vec![Dep::simple("", "^0.1")],
13382 vec![Dep::simple("caixa-teia", "^0.1")],
13383 vec![
13384 Dep::simple("caixa-teia", "^0.1"),
13385 Dep::simple("caixa-core", "^0.1"),
13386 ],
13387 vec![
13388 Dep::simple("caixa-teia", "^0.1"),
13389 Dep::simple("caixa-teia", "^0.2"),
13390 ],
13391 ] {
13392 let c = caixa_with_deps(deps.clone());
13393 assert_eq!(
13394 c.deps(),
13395 deps.as_slice(),
13396 "Caixa::deps must return :deps verbatim (got {:?}, \
13397 expected {deps:?})",
13398 c.deps(),
13399 );
13400 assert_eq!(
13401 c.deps(),
13402 c.deps.as_slice(),
13403 "Caixa::deps must element-equal the raw \
13404 `self.deps.as_slice()` field access across every \
13405 value in the Vec<Dep> accept-set",
13406 );
13407 }
13408 }
13409
13410 #[test]
13411 fn validate_deps_duplicate_arm_routes_through_accessor() {
13412 // Composition pin: [`Caixa::validate_deps`]'s within-`:deps`
13413 // duplicate-`:nome` gate must key off [`Caixa::deps`], not the
13414 // raw `&self.deps` field-borrow walk. Structurally: a `Caixa
13415 // { deps: vec![Dep::simple("d", "^0.1"), Dep::simple("d",
13416 // "^0.2")], .. }` must surface the `DuplicateNome { list:
13417 // ":deps" }` refusal exactly, and a `Caixa { deps: vec![
13418 // Dep::simple("d", "^0.1")], .. }` (the canonical single-entry
13419 // form) must pass validate. The pair jointly pins the accessor +
13420 // validate-gate composition: any future silent detour that had
13421 // the accessor return a dedupped slice on the `[a, a]` arm (a
13422 // `.iter().unique_by(|d| d.nome.as_str()).collect()` collapse)
13423 // would silently absorb the `DuplicateNome` refusal at the
13424 // accessor boundary and the validate gate would accept a
13425 // struct-literal `Caixa` carrying the drift — the composition
13426 // pin catches that at caixa-core build time.
13427 //
13428 // Peer of the per-`Caixa`
13429 // `validate_autores_empty_entry_arm_routes_through_accessor`
13430 // (b5d813f), `validate_etiquetas_empty_entry_arm_routes_through_accessor`
13431 // (78c7d3c), `validate_code_paths_bibliotecas_empty_arm_routes_through_accessor`
13432 // (8a36c23), `validate_code_paths_exe_empty_arm_routes_through_accessor`
13433 // (65d9527), and `validate_code_paths_servicos_empty_arm_routes_through_accessor`
13434 // (611f78b) accessor-composition pins on the sibling `&[T]`-
13435 // composition axes — same "the validate gate must route through
13436 // the substrate-primitive typed dispatch" discipline extended
13437 // onto the sibling outer top-level [`Caixa`] `&[Dep]`-
13438 // composition surface, opening the outer-`Caixa` dependency-slot
13439 // arm of the composition-pin family.
13440 let c = caixa_with_deps(vec![Dep::simple("d", "^0.1"), Dep::simple("d", "^0.2")]);
13441 let err = c.validate_deps().unwrap_err();
13442 assert!(
13443 matches!(
13444 err,
13445 DepError::DuplicateNome { ref nome, list } if nome == "d"
13446 && list == crate::render::DEP_AUTHOR_KEY_DEPS
13447 ),
13448 "validate_deps must reject deps == \
13449 vec![Dep(\"d\",\"^0.1\"), Dep(\"d\",\"^0.2\")] with \
13450 DuplicateNome {{ nome: \"d\", list: \":deps\" }} — the \
13451 accessor and the validate gate must route through the \
13452 same substrate-primitive typed dispatch on the :deps \
13453 within-list duplicate arm (got {err:?})",
13454 );
13455 let c = caixa_with_deps(vec![Dep::simple("d", "^0.1")]);
13456 assert!(
13457 c.validate_deps().is_ok(),
13458 "validate_deps must accept deps == vec![Dep(\"d\",\"^0.1\")] \
13459 (the canonical single-entry form)",
13460 );
13461 }
13462
13463 #[test]
13464 fn deps_projects_slice_by_borrow() {
13465 // The by-borrow pin: [`Caixa::deps`] returns `&[Dep]` by borrow
13466 // — the returned slice borrows the underlying `Vec<Dep>` storage
13467 // of the `:deps` slot and the accessor must not clone the
13468 // backing `Vec` on every call. Peer of the per-`Caixa`
13469 // `autores_projects_slice_by_borrow` (b5d813f),
13470 // `etiquetas_projects_slice_by_borrow` (78c7d3c),
13471 // `bibliotecas_projects_slice_by_borrow` (8a36c23),
13472 // `exe_projects_slice_by_borrow` (65d9527), and
13473 // `servicos_projects_slice_by_borrow` (611f78b) by-borrow pins
13474 // on the sibling outer top-level [`Caixa`] `&[String]`-return
13475 // axes — the accessor's returned slice must borrow from `&self`
13476 // (the returned reference's lifetime is tied to `&self`), and
13477 // calling the accessor twice on the same [`Caixa`] must yield
13478 // slices that are pointer-equal (the underlying byte-buffer is
13479 // the storage `Vec`'s allocation, not a fresh copy) as well as
13480 // value-equal (idempotent, no side effects on `&self`).
13481 //
13482 // Pins against a future silent detour that returned an owned
13483 // `Vec<Dep>` (which would type-check but silently clone on
13484 // every call), a `&Vec<Dep>` return (which would leak the
13485 // backing `Vec`'s grow/push/reserve surface no downstream
13486 // consumer reaches for), or a one-arm-only accessor that
13487 // returned a saturating value on some sentinel input.
13488 for deps in [
13489 vec![],
13490 vec![Dep::simple("caixa-teia", "^0.1")],
13491 vec![
13492 Dep::simple("caixa-teia", "^0.1"),
13493 Dep::simple("caixa-core", "^0.1"),
13494 ],
13495 ] {
13496 let c = caixa_with_deps(deps.clone());
13497 let first = c.deps();
13498 let second = c.deps();
13499 assert_eq!(
13500 first, second,
13501 "Caixa::deps must be idempotent — two successive calls \
13502 on the same &self must return the same &[Dep]",
13503 );
13504 assert_eq!(
13505 first.as_ptr(),
13506 second.as_ptr(),
13507 "Caixa::deps must borrow the underlying Vec<Dep> \
13508 storage — two successive calls must return slices \
13509 with the same backing pointer (a fresh Vec<Dep> clone \
13510 would change the pointer on every call)",
13511 );
13512 assert_eq!(
13513 first,
13514 deps.as_slice(),
13515 "Caixa::deps must return :deps verbatim by borrow — \
13516 got {first:?}, expected {deps:?}",
13517 );
13518 }
13519 }
13520
13521 // ── Caixa::deps_dev — outer top-level &[Dep] slice accessor ──────
13522
13523 fn caixa_with_deps_dev(deps_dev: Vec<Dep>) -> Caixa {
13524 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
13525 c.deps_dev = deps_dev;
13526 c
13527 }
13528
13529 #[test]
13530 fn deps_dev_returns_deps_dev_slice_verbatim_across_permutations() {
13531 // The canonical per-`Caixa` `:deps-dev` universal-axis dev-only-
13532 // dependency-declaration-list slice pin: [`Caixa::deps_dev`]
13533 // must return the `:deps-dev` typed [`Vec<Dep>`] list verbatim as
13534 // a `&[Dep]`, element-equal to the raw `self.deps_dev.as_slice()`
13535 // access across every representative value in the accept-set —
13536 // `[]` (the "no dev deps declared" arm every existing fixture
13537 // without a `:deps-dev` line carries; the [`Caixa::template`]
13538 // scaffold emits `:deps-dev ()`), a canonical single-entry list
13539 // (the shape most consumer caixas carry — a `tatara-check` dev
13540 // pin), a canonical two-entry list (the multi-dev-dep closure),
13541 // and two past-the-guard sentinels — a `[""]`-`:nome` entry
13542 // ([`Self::validate_deps`] rejects through `NomeEmpty` /
13543 // `NomeInvalid` but the accessor must ship the raw slot
13544 // verbatim) and a `[a, a]` duplicate (validate rejects through
13545 // `DuplicateNome { list: ":deps-dev" }` but the accessor must
13546 // ship the raw slot verbatim so struct-literal fixtures continue
13547 // to expose the duplicate at the accessor).
13548 //
13549 // Second outer top-level [`Caixa`] `&[Dep]`-return slice-accessor
13550 // pin on the substrate primitive — closes the outer-`Caixa`
13551 // dependency-slot `&[Dep]` sub-family the sibling
13552 // `deps_returns_deps_slice_verbatim_across_permutations`
13553 // (ad34b4e) opened on. Folds the "outer [`Caixa`] `&[Dep]`
13554 // slice" projection pattern onto the sibling dev-dep axis —
13555 // pins against a future silent detour that returned an owned
13556 // `Vec<Dep>` (which would type-check but silently clone on every
13557 // accessor call, breaking the zero-cost projection every peer
13558 // sibling slice accessor carries), a `[""] → []` collapse (which
13559 // would silently absorb the `NomeEmpty` refusal case at the
13560 // accessor boundary), or a `[a, a] → [a]` dedup collapse (which
13561 // would silently absorb the `DuplicateNome` refusal case at the
13562 // accessor boundary).
13563 for deps_dev in [
13564 vec![],
13565 vec![Dep::simple("", "^0.1")],
13566 vec![Dep::simple("tatara-check", "^0.1")],
13567 vec![
13568 Dep::simple("tatara-check", "^0.1"),
13569 Dep::simple("caixa-lint", "^0.1"),
13570 ],
13571 vec![
13572 Dep::simple("tatara-check", "^0.1"),
13573 Dep::simple("tatara-check", "^0.2"),
13574 ],
13575 ] {
13576 let c = caixa_with_deps_dev(deps_dev.clone());
13577 assert_eq!(
13578 c.deps_dev(),
13579 deps_dev.as_slice(),
13580 "Caixa::deps_dev must return :deps-dev verbatim (got \
13581 {:?}, expected {deps_dev:?})",
13582 c.deps_dev(),
13583 );
13584 assert_eq!(
13585 c.deps_dev(),
13586 c.deps_dev.as_slice(),
13587 "Caixa::deps_dev must element-equal the raw \
13588 `self.deps_dev.as_slice()` field access across every \
13589 value in the Vec<Dep> accept-set",
13590 );
13591 }
13592 }
13593
13594 #[test]
13595 fn validate_deps_duplicate_deps_dev_arm_routes_through_accessor() {
13596 // Composition pin: [`Caixa::validate_deps`]'s within-`:deps-dev`
13597 // duplicate-`:nome` gate must key off [`Caixa::deps_dev`], not
13598 // the raw `&self.deps_dev` field-borrow walk. Structurally: a
13599 // `Caixa { deps_dev: vec![Dep::simple("d", "^0.1"),
13600 // Dep::simple("d", "^0.2")], .. }` must surface the
13601 // `DuplicateNome { list: ":deps-dev" }` refusal exactly, and a
13602 // `Caixa { deps_dev: vec![Dep::simple("d", "^0.1")], .. }` (the
13603 // canonical single-entry form) must pass validate. The pair
13604 // jointly pins the accessor + validate-gate composition: any
13605 // future silent detour that had the accessor return a dedupped
13606 // slice on the `[a, a]` arm (a
13607 // `.iter().unique_by(|d| d.nome.as_str()).collect()` collapse)
13608 // would silently absorb the `DuplicateNome` refusal at the
13609 // accessor boundary and the validate gate would accept a
13610 // struct-literal `Caixa` carrying the drift — the composition
13611 // pin catches that at caixa-core build time.
13612 //
13613 // Peer of `validate_deps_duplicate_arm_routes_through_accessor`
13614 // (ad34b4e) on the sibling `:deps` axis — same "the validate
13615 // gate must route through the substrate-primitive typed
13616 // dispatch" discipline folded onto the sibling `:deps-dev`
13617 // axis, closing the two-list dep-graph composition-pin family.
13618 // The `:deps-dev` diagnostic must carry the
13619 // `DEP_AUTHOR_KEY_DEPS_DEV` list-tag (not
13620 // `DEP_AUTHOR_KEY_DEPS`) so the emitted error names the
13621 // offending list unambiguously.
13622 let c = caixa_with_deps_dev(vec![Dep::simple("d", "^0.1"), Dep::simple("d", "^0.2")]);
13623 let err = c.validate_deps().unwrap_err();
13624 assert!(
13625 matches!(
13626 err,
13627 DepError::DuplicateNome { ref nome, list } if nome == "d"
13628 && list == crate::render::DEP_AUTHOR_KEY_DEPS_DEV
13629 ),
13630 "validate_deps must reject deps_dev == \
13631 vec![Dep(\"d\",\"^0.1\"), Dep(\"d\",\"^0.2\")] with \
13632 DuplicateNome {{ nome: \"d\", list: \":deps-dev\" }} — the \
13633 accessor and the validate gate must route through the \
13634 same substrate-primitive typed dispatch on the :deps-dev \
13635 within-list duplicate arm (got {err:?})",
13636 );
13637 let c = caixa_with_deps_dev(vec![Dep::simple("d", "^0.1")]);
13638 assert!(
13639 c.validate_deps().is_ok(),
13640 "validate_deps must accept deps_dev == \
13641 vec![Dep(\"d\",\"^0.1\")] (the canonical single-entry form)",
13642 );
13643 }
13644
13645 #[test]
13646 fn deps_dev_projects_slice_by_borrow() {
13647 // The by-borrow pin: [`Caixa::deps_dev`] returns `&[Dep]` by
13648 // borrow — the returned slice borrows the underlying `Vec<Dep>`
13649 // storage of the `:deps-dev` slot and the accessor must not
13650 // clone the backing `Vec` on every call. Peer of
13651 // `deps_projects_slice_by_borrow` (ad34b4e) on the sibling
13652 // `:deps` axis, and of the per-`Caixa`
13653 // `autores_projects_slice_by_borrow` (b5d813f),
13654 // `etiquetas_projects_slice_by_borrow` (78c7d3c),
13655 // `bibliotecas_projects_slice_by_borrow` (8a36c23),
13656 // `exe_projects_slice_by_borrow` (65d9527), and
13657 // `servicos_projects_slice_by_borrow` (611f78b) by-borrow pins
13658 // on the sibling outer top-level [`Caixa`] `&[String]`-return
13659 // axes — the accessor's returned slice must borrow from `&self`
13660 // (the returned reference's lifetime is tied to `&self`), and
13661 // calling the accessor twice on the same [`Caixa`] must yield
13662 // slices that are pointer-equal (the underlying byte-buffer is
13663 // the storage `Vec`'s allocation, not a fresh copy) as well as
13664 // value-equal (idempotent, no side effects on `&self`).
13665 //
13666 // Pins against a future silent detour that returned an owned
13667 // `Vec<Dep>` (which would type-check but silently clone on
13668 // every call), a `&Vec<Dep>` return (which would leak the
13669 // backing `Vec`'s grow/push/reserve surface no downstream
13670 // consumer reaches for), or a one-arm-only accessor that
13671 // returned a saturating value on some sentinel input.
13672 for deps_dev in [
13673 vec![],
13674 vec![Dep::simple("tatara-check", "^0.1")],
13675 vec![
13676 Dep::simple("tatara-check", "^0.1"),
13677 Dep::simple("caixa-lint", "^0.1"),
13678 ],
13679 ] {
13680 let c = caixa_with_deps_dev(deps_dev.clone());
13681 let first = c.deps_dev();
13682 let second = c.deps_dev();
13683 assert_eq!(
13684 first, second,
13685 "Caixa::deps_dev must be idempotent — two successive \
13686 calls on the same &self must return the same &[Dep]",
13687 );
13688 assert_eq!(
13689 first.as_ptr(),
13690 second.as_ptr(),
13691 "Caixa::deps_dev must borrow the underlying Vec<Dep> \
13692 storage — two successive calls must return slices \
13693 with the same backing pointer (a fresh Vec<Dep> clone \
13694 would change the pointer on every call)",
13695 );
13696 assert_eq!(
13697 first,
13698 deps_dev.as_slice(),
13699 "Caixa::deps_dev must return :deps-dev verbatim by \
13700 borrow — got {first:?}, expected {deps_dev:?}",
13701 );
13702 }
13703 }
13704
13705 // ── Caixa::limits — outer top-level Option<&LimitsSpec> composite-reference accessor ──
13706
13707 fn caixa_with_limits(limits: Option<crate::LimitsSpec>) -> Caixa {
13708 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
13709 c.limits = limits;
13710 c
13711 }
13712
13713 #[test]
13714 fn limits_returns_limits_option_ref_verbatim_across_permutations() {
13715 // The canonical per-`Caixa` `:limits` M2 typed-slot outer-
13716 // composite optional-composite-reference-shape pin:
13717 // [`Caixa::limits`] must return the `:limits` typed
13718 // `Option<LimitsSpec>` verbatim as an `Option<&LimitsSpec>`
13719 // reference over the same backing storage the raw
13720 // `self.limits.as_ref()` field access borrows from, byte-equal
13721 // across every representative fixture in the accept-set — the
13722 // author-omitted `None` shape (the "engine-default applies"
13723 // partition every downstream Servico M2 overlay emitter treats
13724 // as "emit nothing"), the empty-composite `Some(LimitsSpec {
13725 // .. default })` shape ([`LimitsSpec::is_empty`] holds — every
13726 // per-axis cap is `None`, so the peer M2 overlay emitter's
13727 // `.is_empty()`-gated projection still emits nothing but the
13728 // outer presence-bit is `Some`, so [`Caixa::declared_servico_slots`]
13729 // still pushes the `M2_AUTHOR_KEY_LIMITS` label), a single-axis
13730 // fixture (only `:memory` set — the canonical shape most
13731 // memory-heavy Servicos carry), and a fully-populated composite
13732 // (every per-axis cap set — the canonical shape a
13733 // sandboxed-by-default Servico carries).
13734 //
13735 // Pins against a future silent detour that returned a fresh-
13736 // cloned [`LimitsSpec`] copy (which would type-check via the
13737 // `Clone` impl but silently break every downstream caller that
13738 // relied on the reference sharing the composite's backing
13739 // identity), a reference to an operator-resolved overlay (the
13740 // future per-cluster `:limits-overrides` slot — its resolution
13741 // must land at exactly this accessor body, not silently divert
13742 // the raw slot away from a second consumer), a
13743 // `None` → `Some(LimitsSpec::default)` cluster-default
13744 // projection (which would collapse the load-bearing
13745 // "author-omitted `:limits` ⇒ engine-default applies" partition
13746 // the peer [`crate::render::servico_m2_overlay`] emitter and
13747 // the peer [`Caixa::declared_servico_slots`] enumerator both
13748 // read), or an axis-shuffled projection (a future detour that
13749 // swapped `memory` and `fuel` through the accessor would
13750 // silently split the paired [`crate::StandardLayout::verify`]
13751 // per-`:limits` shape gate's traversal input from the peer
13752 // `servico_m2_overlay` emitter's projection input).
13753 //
13754 // First outer top-level [`Caixa`] `Option<&Composite>`-return
13755 // composite-reference accessor pin on the substrate primitive
13756 // — opens the outer-`Caixa` `Option<&Composite>` composite-
13757 // reference projection pattern the sibling `:behavior`
13758 // [`crate::BehaviorSpec`] / `:politicas`
13759 // [`crate::aplicacao::MeshPolicy`] / `:placement`
13760 // [`crate::aplicacao::Placement`] / `:entrada`
13761 // [`crate::aplicacao::Entrada`] future outer-composite lifts
13762 // fold on. Peer of the closed M3 outer-composite family the
13763 // sibling [`crate::AplicacaoSpec::politicas`] (534dc21) /
13764 // [`crate::AplicacaoSpec::placement`] (9abb8f0) /
13765 // [`crate::AplicacaoSpec::entrada`] (d32111c) composite-
13766 // reference accessor pins already carry on the outer
13767 // [`crate::AplicacaoSpec`] altitude — extends the outer-
13768 // accessor byte-equal-projection discipline onto the outer
13769 // top-level [`Caixa`] M2 Servico-runtime slot altitude.
13770 use crate::LimitsSpec;
13771 use std::time::Duration;
13772 let fixtures: Vec<Option<LimitsSpec>> = vec![
13773 None,
13774 Some(LimitsSpec::default()),
13775 Some(LimitsSpec {
13776 memory: Some(64 * 1024 * 1024),
13777 ..Default::default()
13778 }),
13779 Some(LimitsSpec {
13780 memory: Some(64 * 1024 * 1024),
13781 fuel: Some(1_000_000),
13782 wall_clock: Some(Duration::from_secs(30)),
13783 cpu: Some(500),
13784 }),
13785 ];
13786 for limits in fixtures {
13787 let c = caixa_with_limits(limits.clone());
13788 assert_eq!(
13789 c.limits(),
13790 limits.as_ref(),
13791 "Caixa::limits must return :limits verbatim (got {:?}, \
13792 expected {:?})",
13793 c.limits(),
13794 limits.as_ref(),
13795 );
13796 match (c.limits(), c.limits.as_ref()) {
13797 (Some(a), Some(b)) => assert!(
13798 std::ptr::eq(a, b),
13799 "Caixa::limits accessor and self.limits.as_ref() \
13800 field access must borrow the same backing storage \
13801 — the accessor is the substrate-primitive typed \
13802 dispatch every downstream Servico-M2-overlay \
13803 composite consumer must route through, and a \
13804 reference-identity split would silently break \
13805 every consumer that relied on the borrow sharing \
13806 the composite's storage",
13807 ),
13808 (None, None) => {}
13809 _ => panic!(
13810 "Caixa::limits presence bit must byte-equal \
13811 self.limits.is_some() — a presence-bit drift would \
13812 silently split the paired StandardLayout::verify \
13813 per-`:limits` shape gate's traversal head from \
13814 the peer render::servico_m2_overlay M2 overlay \
13815 emitter's traversal head from the peer \
13816 Caixa::declared_servico_slots M2 declared-slot \
13817 enumerator's presence probe",
13818 ),
13819 }
13820 assert_eq!(
13821 c.limits().is_some(),
13822 c.limits.is_some(),
13823 "Caixa::limits().is_some() must byte-equal \
13824 self.limits.is_some() — a presence-bit drift would \
13825 silently split every downstream Option<&LimitsSpec> \
13826 consumer's partition on the engine-default arm",
13827 );
13828 }
13829 }
13830
13831 #[test]
13832 fn declared_servico_slots_limits_arm_routes_through_accessor() {
13833 // Composition pin: [`Caixa::declared_servico_slots`]'s
13834 // `:limits` presence-probe arm must key off [`Caixa::limits`],
13835 // not the raw `self.limits.is_some()` field-probe. Structurally:
13836 // a `Caixa { limits: Some(LimitsSpec::default()), .. }` must
13837 // still push `M2_AUTHOR_KEY_LIMITS` onto the declared-slot list
13838 // (the presence bit is `Some`, so the M2 kind-coherence gate
13839 // must surface the slot as "declared" even when every per-axis
13840 // cap is unset), and a `Caixa { limits: None, .. }` must NOT
13841 // push the label (the "author omitted the slot entirely"
13842 // partition). The pair jointly pins the accessor + declared-
13843 // slot enumerator composition: any future silent detour that
13844 // had the accessor collapse `Some(LimitsSpec::default())` to
13845 // `None` (a `.filter(|l| !l.is_empty())` projection) would
13846 // silently absorb the "declared but empty" arm at the
13847 // accessor boundary and the [`crate::LayoutError::ServicoSlotsOnNonServico`]
13848 // kind-coherence gate would silently accept a
13849 // struct-literal `Caixa` carrying the drift.
13850 //
13851 // Peer of the sibling per-`Caixa`
13852 // `validate_deps_duplicate_arm_routes_through_accessor` (ad34b4e)
13853 // and `validate_deps_duplicate_deps_dev_arm_routes_through_accessor`
13854 // (f7fd81e) accessor-composition pins on the sibling `:deps` /
13855 // `:deps-dev` outer-`&[Dep]`-composition axes — same "the
13856 // enumerator gate must route through the substrate-primitive
13857 // typed dispatch" discipline extended onto the outer top-level
13858 // [`Caixa`] `Option<&LimitsSpec>`-composition surface, opening
13859 // the outer-`Caixa` M2 Servico-runtime-slot arm of the
13860 // composition-pin family.
13861 use crate::LimitsSpec;
13862 let c = caixa_with_limits(Some(LimitsSpec::default()));
13863 let slots = c.declared_servico_slots();
13864 assert!(
13865 slots.contains(&crate::render::M2_AUTHOR_KEY_LIMITS),
13866 "declared_servico_slots must push M2_AUTHOR_KEY_LIMITS \
13867 when `:limits` is Some (even for LimitsSpec::default()) \
13868 — the accessor and the enumerator gate must route through \
13869 the same substrate-primitive typed dispatch on the outer \
13870 :limits presence bit (got slots={slots:?})",
13871 );
13872 let c = caixa_with_limits(None);
13873 let slots = c.declared_servico_slots();
13874 assert!(
13875 !slots.contains(&crate::render::M2_AUTHOR_KEY_LIMITS),
13876 "declared_servico_slots must NOT push M2_AUTHOR_KEY_LIMITS \
13877 when `:limits` is None — the author-omitted arm must \
13878 route through the accessor's None-return unchanged (got \
13879 slots={slots:?})",
13880 );
13881 }
13882
13883 #[test]
13884 fn servico_m2_overlay_limits_arm_routes_through_accessor() {
13885 // Composition pin: [`crate::render::servico_m2_overlay`]'s
13886 // per-`:limits` M2 overlay emit arm must key off
13887 // [`Caixa::limits`], not the raw `&caixa.limits` field-borrow.
13888 // Structurally: a `Caixa { limits: Some(LimitsSpec { memory:
13889 // Some(64 MiB), .. default }), .. }` must surface the
13890 // `M2_KEY_LIMITS` key with the per-axis
13891 // `memory: "64MiB"` sub-mapping in the overlay, a `Caixa {
13892 // limits: Some(LimitsSpec::default()), .. }` must omit the
13893 // key entirely (the `.is_empty()`-gated inner arm elides an
13894 // empty composite even when the outer presence bit is `Some`),
13895 // and a `Caixa { limits: None, .. }` must also omit the key
13896 // (the "author omitted the slot entirely" partition). The
13897 // three-fixture family jointly pins the accessor + M2 overlay
13898 // emitter composition: any future silent detour that had the
13899 // accessor return a fresh-cloned copy on the `Some` arm (a
13900 // `LimitsSpec::clone()` projection) would silently break the
13901 // reference-identity pin the peer per-axis
13902 // `serde_yaml::to_value(limits)` projection reads from.
13903 use crate::LimitsSpec;
13904 use crate::render::{M2_KEY_LIMITS, servico_m2_overlay};
13905 let c = caixa_with_limits(Some(LimitsSpec {
13906 memory: Some(64 * 1024 * 1024),
13907 ..Default::default()
13908 }));
13909 let overlay = servico_m2_overlay(&c).unwrap();
13910 assert!(
13911 overlay.contains_key(M2_KEY_LIMITS),
13912 "servico_m2_overlay must surface M2_KEY_LIMITS when \
13913 `:limits` carries a non-empty composite — the accessor \
13914 and the M2 overlay emitter must route through the same \
13915 substrate-primitive typed dispatch on the outer :limits \
13916 composite (got overlay={overlay:?})",
13917 );
13918 let c = caixa_with_limits(Some(LimitsSpec::default()));
13919 let overlay = servico_m2_overlay(&c).unwrap();
13920 assert!(
13921 !overlay.contains_key(M2_KEY_LIMITS),
13922 "servico_m2_overlay must omit M2_KEY_LIMITS when \
13923 `:limits` is Some(LimitsSpec::default()) — the empty \
13924 composite's `.is_empty()`-gated inner arm must elide \
13925 the key regardless of the outer presence bit (got \
13926 overlay={overlay:?})",
13927 );
13928 let c = caixa_with_limits(None);
13929 let overlay = servico_m2_overlay(&c).unwrap();
13930 assert!(
13931 !overlay.contains_key(M2_KEY_LIMITS),
13932 "servico_m2_overlay must omit M2_KEY_LIMITS when \
13933 `:limits` is None — the author-omitted arm must route \
13934 through the accessor's None-return unchanged (got \
13935 overlay={overlay:?})",
13936 );
13937 }
13938
13939 #[test]
13940 fn limits_projects_option_ref_by_borrow() {
13941 // The by-borrow pin: [`Caixa::limits`] returns
13942 // `Option<&LimitsSpec>` by borrow — the returned reference
13943 // borrows the underlying `Option<LimitsSpec>` storage of the
13944 // `:limits` slot and the accessor must not clone the backing
13945 // composite on every call. Peer of the sibling
13946 // `deps_projects_slice_by_borrow` (ad34b4e) /
13947 // `deps_dev_projects_slice_by_borrow` (f7fd81e) by-borrow pins
13948 // on the outer top-level [`Caixa`] `&[Dep]`-return axes —
13949 // extended here to the outer [`Caixa`] `Option<&Composite>`-
13950 // return axis: the accessor's returned reference must borrow
13951 // from `&self` (the returned reference's lifetime is tied to
13952 // `&self`), and calling the accessor twice on the same
13953 // [`Caixa`] must yield references that are pointer-equal (the
13954 // underlying byte-buffer is the storage `LimitsSpec`'s
13955 // allocation, not a fresh copy) as well as value-equal
13956 // (idempotent, no side effects on `&self`).
13957 //
13958 // Pins against a future silent detour that returned an owned
13959 // `LimitsSpec` (which would type-check via the `Clone` impl
13960 // but silently clone on every call), a `&LimitsSpec` panic-
13961 // return on the `None` arm (which would collapse the load-
13962 // bearing `Option` presence-bit into a runtime panic), or a
13963 // one-arm-only accessor that returned a saturating composite
13964 // on some sentinel input.
13965 use crate::LimitsSpec;
13966 use std::time::Duration;
13967 for limits in [
13968 Some(LimitsSpec::default()),
13969 Some(LimitsSpec {
13970 memory: Some(64 * 1024 * 1024),
13971 fuel: Some(1_000_000),
13972 wall_clock: Some(Duration::from_secs(30)),
13973 cpu: Some(500),
13974 }),
13975 ] {
13976 let c = caixa_with_limits(limits.clone());
13977 let first = c.limits().unwrap();
13978 let second = c.limits().unwrap();
13979 assert_eq!(
13980 first, second,
13981 "Caixa::limits must be idempotent — two successive \
13982 calls on the same &self must return the same \
13983 &LimitsSpec",
13984 );
13985 assert!(
13986 std::ptr::eq(first, second),
13987 "Caixa::limits must borrow the underlying \
13988 Option<LimitsSpec> storage — two successive calls \
13989 must return references with the same backing pointer \
13990 (a fresh LimitsSpec clone would change the pointer \
13991 on every call)",
13992 );
13993 assert_eq!(
13994 Some(first),
13995 limits.as_ref(),
13996 "Caixa::limits must return :limits verbatim by borrow \
13997 — got {first:?}, expected {:?}",
13998 limits.as_ref(),
13999 );
14000 }
14001 let c = caixa_with_limits(None);
14002 assert!(
14003 c.limits().is_none(),
14004 "Caixa::limits must return None when :limits is absent — \
14005 the author-omitted arm must project through the \
14006 accessor's Option::None unchanged",
14007 );
14008 }
14009
14010 // ── Caixa::behavior — outer top-level Option<&BehaviorSpec> composite-reference accessor ──
14011
14012 fn caixa_with_behavior(behavior: Option<crate::BehaviorSpec>) -> Caixa {
14013 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
14014 c.behavior = behavior;
14015 c
14016 }
14017
14018 #[test]
14019 fn behavior_returns_behavior_option_ref_verbatim_across_permutations() {
14020 // The canonical per-`Caixa` `:behavior` M2 typed-slot outer-
14021 // composite optional-composite-reference-shape pin:
14022 // [`Caixa::behavior`] must return the `:behavior` typed
14023 // `Option<BehaviorSpec>` verbatim as an `Option<&BehaviorSpec>`
14024 // reference over the same backing storage the raw
14025 // `self.behavior.as_ref()` field access borrows from, byte-equal
14026 // across every representative fixture in the accept-set — the
14027 // author-omitted `None` shape (the "runtime-default applies"
14028 // partition every downstream Servico M2 overlay emitter treats
14029 // as "emit nothing"), the empty-composite `Some(BehaviorSpec {
14030 // .. default })` shape ([`BehaviorSpec::is_empty`] holds —
14031 // every per-callback path is `None`, so the peer M2 overlay
14032 // emitter's `.is_empty()`-gated projection still emits nothing
14033 // but the outer presence-bit is `Some`, so
14034 // [`Caixa::declared_servico_slots`] still pushes the
14035 // `M2_AUTHOR_KEY_BEHAVIOR` label), a single-callback fixture
14036 // (only `:on-state-change` set — the canonical shape a caixa
14037 // that only wires the hot-upgrade migration path carries), and
14038 // a fully-populated composite (every per-callback path set —
14039 // the canonical shape a fully-instrumented gen_server-shaped
14040 // Servico carries).
14041 //
14042 // Peer of the sibling
14043 // `limits_returns_limits_option_ref_verbatim_across_permutations`
14044 // (b2bd9d7) opening fixture-family + reference-identity +
14045 // presence-bit tetrad pin on the outer top-level [`Caixa`]
14046 // `Option<&Composite>`-return sub-family — extended here to the
14047 // second axis of that sub-family so both of the currently-lifted
14048 // M2 Servico-runtime `Option<&Composite>` slots (`:limits` /
14049 // `:behavior`) carry the same "byte-equal, borrow-shared,
14050 // presence-bit-preserved" outer-accessor discipline.
14051 //
14052 // Pins against a future silent detour that returned a fresh-
14053 // cloned [`crate::BehaviorSpec`] copy (which would type-check
14054 // via the `Clone` impl but silently break every downstream
14055 // caller that relied on the reference sharing the composite's
14056 // backing identity), a reference to an operator-resolved
14057 // overlay (a future per-cluster `:behavior-overrides` slot —
14058 // its resolution must land at exactly this accessor body, not
14059 // silently divert the raw slot away from a second consumer), a
14060 // `None` → `Some(BehaviorSpec::default)` cluster-default
14061 // projection (which would collapse the load-bearing
14062 // "author-omitted `:behavior` ⇒ runtime-default applies"
14063 // partition the peer [`crate::render::servico_m2_overlay`]
14064 // emitter, the peer [`Caixa::declared_servico_slots`]
14065 // enumerator, and the cross-slot
14066 // [`crate::upgrade::validate_upgrade_from_against_behavior`]
14067 // gate all read), or a callback-shuffled projection (a future
14068 // detour that swapped `on_init` and `on_terminate` through the
14069 // accessor would silently split the paired
14070 // [`crate::StandardLayout::verify`] per-`:behavior` shape gate's
14071 // traversal input from the peer `servico_m2_overlay` emitter's
14072 // projection input from the cross-slot `:state-change`
14073 // composition gate's traversal input).
14074 use crate::BehaviorSpec;
14075 use std::path::PathBuf;
14076 let fixtures: Vec<Option<BehaviorSpec>> = vec![
14077 None,
14078 Some(BehaviorSpec::default()),
14079 Some(BehaviorSpec {
14080 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
14081 ..Default::default()
14082 }),
14083 Some(BehaviorSpec {
14084 on_init: Some(PathBuf::from("lib/init.lisp")),
14085 on_call: Some(PathBuf::from("lib/handlers.lisp")),
14086 on_cast: Some(PathBuf::from("lib/handlers.lisp")),
14087 on_info: Some(PathBuf::from("lib/handlers.lisp")),
14088 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
14089 on_terminate: Some(PathBuf::from("lib/cleanup.lisp")),
14090 }),
14091 ];
14092 for behavior in fixtures {
14093 let c = caixa_with_behavior(behavior.clone());
14094 assert_eq!(
14095 c.behavior(),
14096 behavior.as_ref(),
14097 "Caixa::behavior must return :behavior verbatim (got \
14098 {:?}, expected {:?})",
14099 c.behavior(),
14100 behavior.as_ref(),
14101 );
14102 match (c.behavior(), c.behavior.as_ref()) {
14103 (Some(a), Some(b)) => assert!(
14104 std::ptr::eq(a, b),
14105 "Caixa::behavior accessor and self.behavior.as_ref() \
14106 field access must borrow the same backing storage \
14107 — the accessor is the substrate-primitive typed \
14108 dispatch every downstream Servico-M2-overlay \
14109 composite consumer must route through, and a \
14110 reference-identity split would silently break \
14111 every consumer that relied on the borrow sharing \
14112 the composite's storage",
14113 ),
14114 (None, None) => {}
14115 _ => panic!(
14116 "Caixa::behavior presence bit must byte-equal \
14117 self.behavior.is_some() — a presence-bit drift \
14118 would silently split the paired \
14119 StandardLayout::verify per-`:behavior` shape \
14120 gate's traversal head from the peer \
14121 render::servico_m2_overlay M2 overlay emitter's \
14122 traversal head from the cross-slot \
14123 validate_upgrade_from_against_behavior \
14124 composition gate's traversal head from the peer \
14125 Caixa::declared_servico_slots M2 declared-slot \
14126 enumerator's presence probe",
14127 ),
14128 }
14129 assert_eq!(
14130 c.behavior().is_some(),
14131 c.behavior.is_some(),
14132 "Caixa::behavior().is_some() must byte-equal \
14133 self.behavior.is_some() — a presence-bit drift would \
14134 silently split every downstream Option<&BehaviorSpec> \
14135 consumer's partition on the runtime-default arm",
14136 );
14137 }
14138 }
14139
14140 #[test]
14141 fn declared_servico_slots_behavior_arm_routes_through_accessor() {
14142 // Composition pin: [`Caixa::declared_servico_slots`]'s
14143 // `:behavior` presence-probe arm must key off
14144 // [`Caixa::behavior`], not the raw `self.behavior.is_some()`
14145 // field-probe. Structurally: a `Caixa { behavior:
14146 // Some(BehaviorSpec::default()), .. }` must still push
14147 // `M2_AUTHOR_KEY_BEHAVIOR` onto the declared-slot list (the
14148 // presence bit is `Some`, so the M2 kind-coherence gate must
14149 // surface the slot as "declared" even when every per-callback
14150 // path is unset), and a `Caixa { behavior: None, .. }` must
14151 // NOT push the label (the "author omitted the slot entirely"
14152 // partition). The pair jointly pins the accessor + declared-
14153 // slot enumerator composition: any future silent detour that
14154 // had the accessor collapse `Some(BehaviorSpec::default())`
14155 // to `None` (a `.filter(|b| !b.is_empty())` projection) would
14156 // silently absorb the "declared but empty" arm at the
14157 // accessor boundary and the
14158 // [`crate::LayoutError::ServicoSlotsOnNonServico`]
14159 // kind-coherence gate would silently accept a struct-literal
14160 // `Caixa` carrying the drift.
14161 //
14162 // Peer of the sibling
14163 // `declared_servico_slots_limits_arm_routes_through_accessor`
14164 // (b2bd9d7) composition pin on the sibling `:limits` outer-
14165 // `Option<&LimitsSpec>` arm of the same
14166 // [`Caixa::declared_servico_slots`] M2 declared-slot
14167 // enumerator's traversal — same "the enumerator gate must
14168 // route through the substrate-primitive typed dispatch"
14169 // discipline extended onto the outer top-level [`Caixa`]
14170 // `Option<&BehaviorSpec>`-composition surface.
14171 use crate::BehaviorSpec;
14172 let c = caixa_with_behavior(Some(BehaviorSpec::default()));
14173 let slots = c.declared_servico_slots();
14174 assert!(
14175 slots.contains(&crate::render::M2_AUTHOR_KEY_BEHAVIOR),
14176 "declared_servico_slots must push M2_AUTHOR_KEY_BEHAVIOR \
14177 when `:behavior` is Some (even for BehaviorSpec::default()) \
14178 — the accessor and the enumerator gate must route through \
14179 the same substrate-primitive typed dispatch on the outer \
14180 :behavior presence bit (got slots={slots:?})",
14181 );
14182 let c = caixa_with_behavior(None);
14183 let slots = c.declared_servico_slots();
14184 assert!(
14185 !slots.contains(&crate::render::M2_AUTHOR_KEY_BEHAVIOR),
14186 "declared_servico_slots must NOT push M2_AUTHOR_KEY_BEHAVIOR \
14187 when `:behavior` is None — the author-omitted arm must \
14188 route through the accessor's None-return unchanged (got \
14189 slots={slots:?})",
14190 );
14191 }
14192
14193 #[test]
14194 fn servico_m2_overlay_behavior_arm_routes_through_accessor() {
14195 // Composition pin: [`crate::render::servico_m2_overlay`]'s
14196 // per-`:behavior` M2 overlay emit arm must key off
14197 // [`Caixa::behavior`], not the raw `&caixa.behavior`
14198 // field-borrow. Structurally: a `Caixa { behavior:
14199 // Some(BehaviorSpec { on_state_change: Some(...), .. default
14200 // }), .. }` must surface the `M2_KEY_BEHAVIOR` key with the
14201 // per-callback `onStateChange` sub-mapping in the overlay, a
14202 // `Caixa { behavior: Some(BehaviorSpec::default()), .. }`
14203 // must omit the key entirely (the `.is_empty()`-gated inner
14204 // arm elides an empty composite even when the outer presence
14205 // bit is `Some`), and a `Caixa { behavior: None, .. }` must
14206 // also omit the key (the "author omitted the slot entirely"
14207 // partition). The three-fixture family jointly pins the
14208 // accessor + M2 overlay emitter composition: any future
14209 // silent detour that had the accessor return a fresh-cloned
14210 // copy on the `Some` arm (a `BehaviorSpec::clone()`
14211 // projection) would silently break the reference-identity
14212 // pin the peer per-callback `serde_yaml::to_value(behavior)`
14213 // projection reads from.
14214 //
14215 // Peer of the sibling
14216 // `servico_m2_overlay_limits_arm_routes_through_accessor`
14217 // (b2bd9d7) composition pin on the sibling `:limits` outer-
14218 // `Option<&LimitsSpec>` arm of the same
14219 // [`crate::render::servico_m2_overlay`] M2 overlay emitter's
14220 // traversal — same "the emitter must route through the
14221 // substrate-primitive typed dispatch on the outer composite"
14222 // discipline extended onto the outer top-level [`Caixa`]
14223 // `Option<&BehaviorSpec>`-composition surface.
14224 use crate::BehaviorSpec;
14225 use crate::render::{M2_KEY_BEHAVIOR, servico_m2_overlay};
14226 use std::path::PathBuf;
14227 let c = caixa_with_behavior(Some(BehaviorSpec {
14228 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
14229 ..Default::default()
14230 }));
14231 let overlay = servico_m2_overlay(&c).unwrap();
14232 assert!(
14233 overlay.contains_key(M2_KEY_BEHAVIOR),
14234 "servico_m2_overlay must surface M2_KEY_BEHAVIOR when \
14235 `:behavior` carries a non-empty composite — the accessor \
14236 and the M2 overlay emitter must route through the same \
14237 substrate-primitive typed dispatch on the outer :behavior \
14238 composite (got overlay={overlay:?})",
14239 );
14240 let c = caixa_with_behavior(Some(BehaviorSpec::default()));
14241 let overlay = servico_m2_overlay(&c).unwrap();
14242 assert!(
14243 !overlay.contains_key(M2_KEY_BEHAVIOR),
14244 "servico_m2_overlay must omit M2_KEY_BEHAVIOR when \
14245 `:behavior` is Some(BehaviorSpec::default()) — the empty \
14246 composite's `.is_empty()`-gated inner arm must elide the \
14247 key regardless of the outer presence bit (got \
14248 overlay={overlay:?})",
14249 );
14250 let c = caixa_with_behavior(None);
14251 let overlay = servico_m2_overlay(&c).unwrap();
14252 assert!(
14253 !overlay.contains_key(M2_KEY_BEHAVIOR),
14254 "servico_m2_overlay must omit M2_KEY_BEHAVIOR when \
14255 `:behavior` is None — the author-omitted arm must route \
14256 through the accessor's None-return unchanged (got \
14257 overlay={overlay:?})",
14258 );
14259 }
14260
14261 #[test]
14262 fn behavior_projects_option_ref_by_borrow() {
14263 // The by-borrow pin: [`Caixa::behavior`] returns
14264 // `Option<&BehaviorSpec>` by borrow — the returned reference
14265 // borrows the underlying `Option<BehaviorSpec>` storage of the
14266 // `:behavior` slot and the accessor must not clone the backing
14267 // composite on every call. Peer of the sibling
14268 // `limits_projects_option_ref_by_borrow` (b2bd9d7) by-borrow
14269 // pin on the outer top-level [`Caixa`] `Option<&Composite>`-
14270 // return sub-family — extended here to the second axis of the
14271 // same sub-family: the accessor's returned reference must
14272 // borrow from `&self` (the returned reference's lifetime is
14273 // tied to `&self`), and calling the accessor twice on the same
14274 // [`Caixa`] must yield references that are pointer-equal (the
14275 // underlying byte-buffer is the storage `BehaviorSpec`'s
14276 // allocation, not a fresh copy) as well as value-equal
14277 // (idempotent, no side effects on `&self`).
14278 //
14279 // Pins against a future silent detour that returned an owned
14280 // `BehaviorSpec` (which would type-check via the `Clone` impl
14281 // but silently clone on every call), a `&BehaviorSpec` panic-
14282 // return on the `None` arm (which would collapse the load-
14283 // bearing `Option` presence-bit into a runtime panic), or a
14284 // one-arm-only accessor that returned a saturating composite
14285 // on some sentinel input.
14286 use crate::BehaviorSpec;
14287 use std::path::PathBuf;
14288 for behavior in [
14289 Some(BehaviorSpec::default()),
14290 Some(BehaviorSpec {
14291 on_init: Some(PathBuf::from("lib/init.lisp")),
14292 on_call: Some(PathBuf::from("lib/handlers.lisp")),
14293 on_cast: Some(PathBuf::from("lib/handlers.lisp")),
14294 on_info: Some(PathBuf::from("lib/handlers.lisp")),
14295 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
14296 on_terminate: Some(PathBuf::from("lib/cleanup.lisp")),
14297 }),
14298 ] {
14299 let c = caixa_with_behavior(behavior.clone());
14300 let first = c.behavior().unwrap();
14301 let second = c.behavior().unwrap();
14302 assert_eq!(
14303 first, second,
14304 "Caixa::behavior must be idempotent — two successive \
14305 calls on the same &self must return the same \
14306 &BehaviorSpec",
14307 );
14308 assert!(
14309 std::ptr::eq(first, second),
14310 "Caixa::behavior must borrow the underlying \
14311 Option<BehaviorSpec> storage — two successive calls \
14312 must return references with the same backing pointer \
14313 (a fresh BehaviorSpec clone would change the pointer \
14314 on every call)",
14315 );
14316 assert_eq!(
14317 Some(first),
14318 behavior.as_ref(),
14319 "Caixa::behavior must return :behavior verbatim by \
14320 borrow — got {first:?}, expected {:?}",
14321 behavior.as_ref(),
14322 );
14323 }
14324 let c = caixa_with_behavior(None);
14325 assert!(
14326 c.behavior().is_none(),
14327 "Caixa::behavior must return None when :behavior is absent \
14328 — the author-omitted arm must project through the \
14329 accessor's Option::None unchanged",
14330 );
14331 }
14332
14333 // ── Caixa::politicas — outer top-level Option<&MeshPolicy> composite-reference accessor ──
14334
14335 fn caixa_aplicacao_with_politicas(politicas: Option<crate::aplicacao::MeshPolicy>) -> Caixa {
14336 use crate::aplicacao::{Membro, WitContract};
14337 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
14338 c.kind = CaixaKind::Aplicacao;
14339 c.membros = vec![Membro {
14340 caixa: "a".into(),
14341 versao: "^0.1".into(),
14342 }];
14343 c.contratos = vec![WitContract {
14344 de: "a".into(),
14345 para: "a".into(),
14346 wit: "wasi:http/proxy".into(),
14347 endpoint: Some("/x".into()),
14348 subject: None,
14349 slot: None,
14350 }];
14351 c.politicas = politicas;
14352 c
14353 }
14354
14355 #[test]
14356 fn politicas_returns_politicas_option_ref_verbatim_across_permutations() {
14357 // The canonical per-`Caixa` `:politicas` M3 mesh-slot outer-
14358 // composite optional-composite-reference-shape pin:
14359 // [`Caixa::politicas`] must return the `:politicas` typed
14360 // `Option<MeshPolicy>` verbatim as an `Option<&MeshPolicy>`
14361 // reference over the same backing storage the raw
14362 // `self.politicas.as_ref()` field access borrows from,
14363 // byte-equal across every representative fixture in the
14364 // accept-set — the author-omitted `None` shape (the "cluster-
14365 // default applies" partition every downstream mesh-artifact
14366 // emitter treats as "emit no `:politicas` overlay"), the
14367 // empty-composite `Some(MeshPolicy { .. default })` shape
14368 // ([`crate::aplicacao::MeshPolicy::is_empty`] holds — every
14369 // per-axis mesh-policy scalar is `None`, so the peer inner
14370 // [`crate::AplicacaoSpec::politicas`] `.is_empty()`-gated
14371 // caixa-mesh overlay elides every per-axis emit but the outer
14372 // presence-bit is `Some`, so [`Caixa::declared_mesh_slots`]
14373 // still pushes the `M3_AUTHOR_KEY_POLITICAS` label), a
14374 // single-axis fixture (only `:timeout` set — the canonical
14375 // shape a latency-sensitive Aplicacao carries), and a
14376 // fully-populated composite (every per-axis mesh-policy
14377 // scalar set — the canonical shape a fully-governed
14378 // Aplicacao carries).
14379 //
14380 // Pins against a future silent detour that returned a fresh-
14381 // cloned [`crate::aplicacao::MeshPolicy`] copy (which would
14382 // type-check via the `Clone` impl but silently break every
14383 // downstream caller that relied on the reference sharing the
14384 // composite's backing identity), a reference to an operator-
14385 // resolved overlay (the future per-cluster
14386 // `:politicas-overrides` slot — its resolution must land at
14387 // exactly this accessor body, not silently divert the raw
14388 // slot away from the peer [`Caixa::declared_mesh_slots`]
14389 // enumerator's presence probe), a
14390 // `None` → `Some(MeshPolicy::default)` cluster-default
14391 // projection (which would collapse the load-bearing
14392 // "author-omitted `:politicas` ⇒ cluster-default applies"
14393 // partition the peer [`Caixa::declared_mesh_slots`]
14394 // enumerator and the peer [`Caixa::aplicacao_view`]
14395 // Aplicacao-composition seed both read), or an axis-shuffled
14396 // projection (a future detour that swapped `timeout` and
14397 // `retries` through the accessor would silently split the
14398 // paired [`Caixa::aplicacao_view`] seed's fold input from the
14399 // sibling M3 mesh-artifact emitter's projection input).
14400 //
14401 // Third outer top-level [`Caixa`] `Option<&Composite>`-return
14402 // composite-reference accessor pin on the substrate primitive
14403 // — peer of the sibling
14404 // `limits_returns_limits_option_ref_verbatim_across_permutations`
14405 // (b2bd9d7) and
14406 // `behavior_returns_behavior_option_ref_verbatim_across_permutations`
14407 // (35d8b52) opening tetrad pins on the outer top-level
14408 // [`Caixa`] `Option<&Composite>`-return sub-family — extended
14409 // here to the first of the three M3 mesh-slot axes so the
14410 // opening third of the outer `Option<&Composite>` sub-family
14411 // carries the same "byte-equal, borrow-shared, presence-bit-
14412 // preserved" outer-accessor discipline.
14413 use crate::aplicacao::{CircuitBreaker, MeshPolicy, RateLimit};
14414 use std::time::Duration;
14415 let fixtures: Vec<Option<MeshPolicy>> = vec![
14416 None,
14417 Some(MeshPolicy::default()),
14418 Some(MeshPolicy {
14419 timeout: Some(Duration::from_secs(30)),
14420 ..Default::default()
14421 }),
14422 Some(MeshPolicy {
14423 timeout: Some(Duration::from_secs(30)),
14424 retries: Some(3),
14425 circuit_breaker: Some(CircuitBreaker {
14426 max_failures: 5,
14427 window: Duration::from_secs(60),
14428 }),
14429 mtls_required: Some(true),
14430 rate_limit: Some(RateLimit {
14431 rate: 100,
14432 window: Duration::from_secs(1),
14433 }),
14434 }),
14435 ];
14436 for politicas in fixtures {
14437 let c = caixa_aplicacao_with_politicas(politicas.clone());
14438 assert_eq!(
14439 c.politicas(),
14440 politicas.as_ref(),
14441 "Caixa::politicas must return :politicas verbatim (got \
14442 {:?}, expected {:?})",
14443 c.politicas(),
14444 politicas.as_ref(),
14445 );
14446 match (c.politicas(), c.politicas.as_ref()) {
14447 (Some(a), Some(b)) => assert!(
14448 std::ptr::eq(a, b),
14449 "Caixa::politicas accessor and self.politicas.as_ref() \
14450 field access must borrow the same backing storage \
14451 — the accessor is the substrate-primitive typed \
14452 dispatch every downstream Aplicacao-mesh-overlay \
14453 composite consumer must route through, and a \
14454 reference-identity split would silently break \
14455 every consumer that relied on the borrow sharing \
14456 the composite's storage",
14457 ),
14458 (None, None) => {}
14459 _ => panic!(
14460 "Caixa::politicas presence bit must byte-equal \
14461 self.politicas.is_some() — a presence-bit drift \
14462 would silently split the paired \
14463 Caixa::aplicacao_view Aplicacao-composition seed's \
14464 traversal head from the peer \
14465 Caixa::declared_mesh_slots M3 declared-slot \
14466 enumerator's presence probe",
14467 ),
14468 }
14469 assert_eq!(
14470 c.politicas().is_some(),
14471 c.politicas.is_some(),
14472 "Caixa::politicas().is_some() must byte-equal \
14473 self.politicas.is_some() — a presence-bit drift would \
14474 silently split every downstream Option<&MeshPolicy> \
14475 consumer's partition on the cluster-default arm",
14476 );
14477 }
14478 }
14479
14480 #[test]
14481 fn declared_mesh_slots_politicas_arm_routes_through_accessor() {
14482 // Composition pin: [`Caixa::declared_mesh_slots`]'s
14483 // `:politicas` presence-probe arm must key off
14484 // [`Caixa::politicas`], not the raw `self.politicas.is_some()`
14485 // field-probe. Structurally: a `Caixa { politicas:
14486 // Some(MeshPolicy::default()), .. }` must still push
14487 // `M3_AUTHOR_KEY_POLITICAS` onto the declared-slot list (the
14488 // presence bit is `Some`, so the M3 kind-coherence gate must
14489 // surface the slot as "declared" even when every per-axis
14490 // scalar is unset), and a `Caixa { politicas: None, .. }` must
14491 // NOT push the label (the "author omitted the slot entirely"
14492 // partition). The pair jointly pins the accessor + declared-
14493 // slot enumerator composition: any future silent detour that
14494 // had the accessor collapse `Some(MeshPolicy::default())` to
14495 // `None` (a `.filter(|p| !p.is_empty())` projection) would
14496 // silently absorb the "declared but empty" arm at the
14497 // accessor boundary and the
14498 // [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
14499 // coherence gate would silently accept a struct-literal
14500 // `Caixa` carrying the drift.
14501 //
14502 // Peer of the sibling
14503 // `declared_servico_slots_limits_arm_routes_through_accessor`
14504 // (b2bd9d7) and
14505 // `declared_servico_slots_behavior_arm_routes_through_accessor`
14506 // (35d8b52) composition pins on the sibling `:limits` /
14507 // `:behavior` outer-`Option<&Composite>` arms of the peer
14508 // [`Caixa::declared_servico_slots`] M2 declared-slot
14509 // enumerator's traversal — same "the enumerator gate must
14510 // route through the substrate-primitive typed dispatch"
14511 // discipline extended onto the outer top-level [`Caixa`] M3
14512 // mesh-slot family so the [`Caixa::declared_mesh_slots`]
14513 // enumerator carries the same routing invariant as its M2
14514 // sibling.
14515 use crate::aplicacao::MeshPolicy;
14516 let c = caixa_aplicacao_with_politicas(Some(MeshPolicy::default()));
14517 let slots = c.declared_mesh_slots();
14518 assert!(
14519 slots.contains(&crate::render::M3_AUTHOR_KEY_POLITICAS),
14520 "declared_mesh_slots must push M3_AUTHOR_KEY_POLITICAS \
14521 when `:politicas` is Some (even for MeshPolicy::default()) \
14522 — the accessor and the enumerator gate must route through \
14523 the same substrate-primitive typed dispatch on the outer \
14524 :politicas presence bit (got slots={slots:?})",
14525 );
14526 let c = caixa_aplicacao_with_politicas(None);
14527 let slots = c.declared_mesh_slots();
14528 assert!(
14529 !slots.contains(&crate::render::M3_AUTHOR_KEY_POLITICAS),
14530 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_POLITICAS \
14531 when `:politicas` is None — the author-omitted arm must \
14532 route through the accessor's None-return unchanged (got \
14533 slots={slots:?})",
14534 );
14535 }
14536
14537 #[test]
14538 fn aplicacao_view_politicas_arm_folds_through_accessor() {
14539 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:politicas`
14540 // Aplicacao-composition seed must fold through
14541 // [`Caixa::politicas`], not the raw
14542 // `self.politicas.clone().unwrap_or_default()` field-borrow.
14543 // Structurally: a `Caixa { politicas: Some(MeshPolicy {
14544 // timeout: Some(30s), .. default }), kind: Aplicacao, .. }`
14545 // must surface a projected [`crate::AplicacaoSpec`] whose
14546 // `politicas().timeout()` field byte-equals the outer
14547 // composite's `timeout` scalar (the fold must project the
14548 // authored composite verbatim), a `Caixa { politicas:
14549 // Some(MeshPolicy::default()), kind: Aplicacao, .. }` must
14550 // surface an [`crate::AplicacaoSpec`] whose `politicas()`
14551 // byte-equals [`crate::aplicacao::MeshPolicy::default`] (the
14552 // fold's empty-composite arm collapses to the same default the
14553 // author-omitted arm does), and a `Caixa { politicas: None,
14554 // kind: Aplicacao, .. }` must surface an
14555 // [`crate::AplicacaoSpec`] whose `politicas()` byte-equals
14556 // [`crate::aplicacao::MeshPolicy::default`] (the "author
14557 // omitted the slot entirely" arm folds through the
14558 // `unwrap_or_default` onto the cluster-default). The triad
14559 // jointly pins the accessor + Aplicacao-composition seed
14560 // composition: any future silent detour that had the accessor
14561 // divert the raw slot away from the seed's fold (an operator-
14562 // resolved overlay's default-fold arm silently differing from
14563 // the raw slot's default-fold arm) would silently split the
14564 // build-time mesh-artifact emission gate from the caixa-mesh
14565 // renderer's Aplicacao-view input at the composition boundary.
14566 use crate::aplicacao::MeshPolicy;
14567 use std::time::Duration;
14568 let c = caixa_aplicacao_with_politicas(Some(MeshPolicy {
14569 timeout: Some(Duration::from_secs(30)),
14570 ..Default::default()
14571 }));
14572 let view = c.aplicacao_view().unwrap();
14573 assert_eq!(
14574 view.politicas().timeout(),
14575 Some(Duration::from_secs(30)),
14576 "Caixa::aplicacao_view must fold the authored :politicas \
14577 :timeout scalar through the accessor verbatim onto the \
14578 projected AplicacaoSpec — a future silent detour at the \
14579 seed's fold arm would surface here as a projected-scalar \
14580 drift (got {:?})",
14581 view.politicas().timeout(),
14582 );
14583 let c = caixa_aplicacao_with_politicas(Some(MeshPolicy::default()));
14584 let view = c.aplicacao_view().unwrap();
14585 assert_eq!(
14586 view.politicas(),
14587 &MeshPolicy::default(),
14588 "Caixa::aplicacao_view must fold Some(MeshPolicy::default()) \
14589 through the accessor onto MeshPolicy::default — the empty- \
14590 composite arm collapses to the same default the author- \
14591 omitted arm does (got {:?})",
14592 view.politicas(),
14593 );
14594 let c = caixa_aplicacao_with_politicas(None);
14595 let view = c.aplicacao_view().unwrap();
14596 assert_eq!(
14597 view.politicas(),
14598 &MeshPolicy::default(),
14599 "Caixa::aplicacao_view must fold None through the accessor's \
14600 unwrap_or_default onto MeshPolicy::default — the author- \
14601 omitted arm must route through the accessor's None-return \
14602 unchanged (got {:?})",
14603 view.politicas(),
14604 );
14605 }
14606
14607 #[test]
14608 fn politicas_projects_option_ref_by_borrow() {
14609 // The by-borrow pin: [`Caixa::politicas`] returns
14610 // `Option<&MeshPolicy>` by borrow — the returned reference
14611 // borrows the underlying `Option<MeshPolicy>` storage of the
14612 // `:politicas` slot and the accessor must not clone the
14613 // backing composite on every call. Peer of the sibling
14614 // `limits_projects_option_ref_by_borrow` (b2bd9d7) and
14615 // `behavior_projects_option_ref_by_borrow` (35d8b52) by-borrow
14616 // pins on the outer top-level [`Caixa`]
14617 // `Option<&Composite>`-return sub-family — extended here to
14618 // the third axis of the same sub-family: the accessor's
14619 // returned reference must borrow from `&self` (the returned
14620 // reference's lifetime is tied to `&self`), and calling the
14621 // accessor twice on the same [`Caixa`] must yield references
14622 // that are pointer-equal (the underlying byte-buffer is the
14623 // storage `MeshPolicy`'s allocation, not a fresh copy) as
14624 // well as value-equal (idempotent, no side effects on
14625 // `&self`).
14626 //
14627 // Pins against a future silent detour that returned an owned
14628 // `MeshPolicy` (which would type-check via the `Clone` impl
14629 // but silently clone on every call), a `&MeshPolicy` panic-
14630 // return on the `None` arm (which would collapse the load-
14631 // bearing `Option` presence-bit into a runtime panic), or a
14632 // one-arm-only accessor that returned a saturating composite
14633 // on some sentinel input.
14634 use crate::aplicacao::{CircuitBreaker, MeshPolicy, RateLimit};
14635 use std::time::Duration;
14636 for politicas in [
14637 Some(MeshPolicy::default()),
14638 Some(MeshPolicy {
14639 timeout: Some(Duration::from_secs(30)),
14640 retries: Some(3),
14641 circuit_breaker: Some(CircuitBreaker {
14642 max_failures: 5,
14643 window: Duration::from_secs(60),
14644 }),
14645 mtls_required: Some(true),
14646 rate_limit: Some(RateLimit {
14647 rate: 100,
14648 window: Duration::from_secs(1),
14649 }),
14650 }),
14651 ] {
14652 let c = caixa_aplicacao_with_politicas(politicas.clone());
14653 let first = c.politicas().unwrap();
14654 let second = c.politicas().unwrap();
14655 assert_eq!(
14656 first, second,
14657 "Caixa::politicas must be idempotent — two successive \
14658 calls on the same &self must return the same \
14659 &MeshPolicy",
14660 );
14661 assert!(
14662 std::ptr::eq(first, second),
14663 "Caixa::politicas must borrow the underlying \
14664 Option<MeshPolicy> storage — two successive calls \
14665 must return references with the same backing pointer \
14666 (a fresh MeshPolicy clone would change the pointer on \
14667 every call)",
14668 );
14669 assert_eq!(
14670 Some(first),
14671 politicas.as_ref(),
14672 "Caixa::politicas must return :politicas verbatim by \
14673 borrow — got {first:?}, expected {:?}",
14674 politicas.as_ref(),
14675 );
14676 }
14677 let c = caixa_aplicacao_with_politicas(None);
14678 assert!(
14679 c.politicas().is_none(),
14680 "Caixa::politicas must return None when :politicas is \
14681 absent — the author-omitted arm must project through the \
14682 accessor's Option::None unchanged",
14683 );
14684 }
14685
14686 // ── Caixa::placement — outer top-level Option<&Placement> composite-reference accessor ──
14687
14688 fn caixa_aplicacao_with_placement(placement: Option<crate::aplicacao::Placement>) -> Caixa {
14689 use crate::aplicacao::{Membro, WitContract};
14690 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
14691 c.kind = CaixaKind::Aplicacao;
14692 c.membros = vec![Membro {
14693 caixa: "a".into(),
14694 versao: "^0.1".into(),
14695 }];
14696 c.contratos = vec![WitContract {
14697 de: "a".into(),
14698 para: "a".into(),
14699 wit: "wasi:http/proxy".into(),
14700 endpoint: Some("/x".into()),
14701 subject: None,
14702 slot: None,
14703 }];
14704 c.placement = placement;
14705 c
14706 }
14707
14708 #[test]
14709 fn placement_returns_placement_option_ref_verbatim_across_permutations() {
14710 // The canonical per-`Caixa` `:placement` M3 mesh-slot outer-
14711 // composite optional-composite-reference-shape pin:
14712 // [`Caixa::placement`] must return the `:placement` typed
14713 // `Option<Placement>` verbatim as an `Option<&Placement>`
14714 // reference over the same backing storage the raw
14715 // `self.placement.as_ref()` field access borrows from,
14716 // byte-equal across every representative fixture in the
14717 // accept-set — the author-omitted `None` shape (the
14718 // "cluster-default applies" partition every downstream mesh-
14719 // artifact emitter treats as "emit no `:placement` overlay"),
14720 // the empty-composite `Some(Placement { .. default })` shape
14721 // (`estrategia: SingleNode`, empty clusters, no shard-key /
14722 // affinity — the outer presence-bit is `Some` so
14723 // [`Caixa::declared_mesh_slots`] still pushes the
14724 // `M3_AUTHOR_KEY_PLACEMENT` label), a single-axis
14725 // `Replicated`-on-two-clusters fixture (the canonical shape a
14726 // stateless HTTP Aplicacao carries), and a fully-populated
14727 // `Sharded`-with-shard-key-and-affinity fixture (the canonical
14728 // shape a stateful Akka-style cluster-sharding Aplicacao
14729 // carries).
14730 //
14731 // Pins against a future silent detour that returned a fresh-
14732 // cloned [`crate::aplicacao::Placement`] copy (which would
14733 // type-check via the `Clone` impl but silently break every
14734 // downstream caller that relied on the reference sharing the
14735 // composite's backing identity), a reference to an operator-
14736 // resolved overlay (the future per-cluster
14737 // `:placement-overrides` slot — its resolution must land at
14738 // exactly this accessor body, not silently divert the raw
14739 // slot away from the peer [`Caixa::declared_mesh_slots`]
14740 // enumerator's presence probe), a `None` →
14741 // `Some(Placement::default)` cluster-default projection (which
14742 // would collapse the load-bearing "author-omitted `:placement`
14743 // ⇒ cluster-default applies" partition the peer
14744 // [`Caixa::declared_mesh_slots`] enumerator and the peer
14745 // [`Caixa::aplicacao_view`] Aplicacao-composition seed both
14746 // read), or an axis-shuffled projection (a future detour that
14747 // swapped `clusters` and `affinity` through the accessor would
14748 // silently split the paired [`Caixa::aplicacao_view`] seed's
14749 // fold input from the sibling M3 mesh-artifact emitter's
14750 // projection input).
14751 //
14752 // Fourth outer top-level [`Caixa`] `Option<&Composite>`-return
14753 // composite-reference accessor pin on the substrate primitive
14754 // — peer of the sibling
14755 // `limits_returns_limits_option_ref_verbatim_across_permutations`
14756 // (b2bd9d7),
14757 // `behavior_returns_behavior_option_ref_verbatim_across_permutations`
14758 // (35d8b52), and
14759 // `politicas_returns_politicas_option_ref_verbatim_across_permutations`
14760 // (5d23d29) opening triad pins on the outer top-level
14761 // [`Caixa`] `Option<&Composite>`-return sub-family — extended
14762 // here to the second of the three M3 mesh-slot axes so the
14763 // opening four-fifths of the outer `Option<&Composite>` sub-
14764 // family carries the same "byte-equal, borrow-shared,
14765 // presence-bit-preserved" outer-accessor discipline.
14766 use crate::aplicacao::{Placement, PlacementStrategy};
14767 let fixtures: Vec<Option<Placement>> = vec![
14768 None,
14769 Some(Placement::default()),
14770 Some(Placement {
14771 estrategia: PlacementStrategy::Replicated,
14772 clusters: vec!["rio".into(), "sao-paulo".into()],
14773 affinity: None,
14774 shard_key: None,
14775 }),
14776 Some(Placement {
14777 estrategia: PlacementStrategy::Sharded,
14778 clusters: vec!["rio".into(), "sao-paulo".into(), "brasilia".into()],
14779 affinity: Some("data-locality".into()),
14780 shard_key: Some("$tenantId".into()),
14781 }),
14782 ];
14783 for placement in fixtures {
14784 let c = caixa_aplicacao_with_placement(placement.clone());
14785 assert_eq!(
14786 c.placement(),
14787 placement.as_ref(),
14788 "Caixa::placement must return :placement verbatim (got \
14789 {:?}, expected {:?})",
14790 c.placement(),
14791 placement.as_ref(),
14792 );
14793 match (c.placement(), c.placement.as_ref()) {
14794 (Some(a), Some(b)) => assert!(
14795 std::ptr::eq(a, b),
14796 "Caixa::placement accessor and self.placement.as_ref() \
14797 field access must borrow the same backing storage \
14798 — the accessor is the substrate-primitive typed \
14799 dispatch every downstream Aplicacao-distribution- \
14800 overlay composite consumer must route through, and \
14801 a reference-identity split would silently break \
14802 every consumer that relied on the borrow sharing \
14803 the composite's storage",
14804 ),
14805 (None, None) => {}
14806 _ => panic!(
14807 "Caixa::placement presence bit must byte-equal \
14808 self.placement.is_some() — a presence-bit drift \
14809 would silently split the paired \
14810 Caixa::aplicacao_view Aplicacao-composition seed's \
14811 traversal head from the peer \
14812 Caixa::declared_mesh_slots M3 declared-slot \
14813 enumerator's presence probe",
14814 ),
14815 }
14816 assert_eq!(
14817 c.placement().is_some(),
14818 c.placement.is_some(),
14819 "Caixa::placement().is_some() must byte-equal \
14820 self.placement.is_some() — a presence-bit drift would \
14821 silently split every downstream Option<&Placement> \
14822 consumer's partition on the cluster-default arm",
14823 );
14824 }
14825 }
14826
14827 #[test]
14828 fn declared_mesh_slots_placement_arm_routes_through_accessor() {
14829 // Composition pin: [`Caixa::declared_mesh_slots`]'s
14830 // `:placement` presence-probe arm must key off
14831 // [`Caixa::placement`], not the raw `self.placement.is_some()`
14832 // field-probe. Structurally: a `Caixa { placement:
14833 // Some(Placement::default()), .. }` must still push
14834 // `M3_AUTHOR_KEY_PLACEMENT` onto the declared-slot list (the
14835 // presence bit is `Some`, so the M3 kind-coherence gate must
14836 // surface the slot as "declared" even when every per-axis
14837 // scalar defers to the cluster-default arm), and a `Caixa {
14838 // placement: None, .. }` must NOT push the label (the "author
14839 // omitted the slot entirely" partition). The pair jointly pins
14840 // the accessor + declared-slot enumerator composition: any
14841 // future silent detour that had the accessor collapse
14842 // `Some(Placement::default())` to `None` (a `.filter(|p|
14843 // p.clusters().is_empty().not())` projection) would silently
14844 // absorb the "declared but empty" arm at the accessor boundary
14845 // and the [`crate::LayoutError::MeshSlotsOnNonAplicacao`]
14846 // kind-coherence gate would silently accept a struct-literal
14847 // `Caixa` carrying the drift.
14848 //
14849 // Peer of the sibling
14850 // `declared_servico_slots_limits_arm_routes_through_accessor`
14851 // (b2bd9d7),
14852 // `declared_servico_slots_behavior_arm_routes_through_accessor`
14853 // (35d8b52), and
14854 // `declared_mesh_slots_politicas_arm_routes_through_accessor`
14855 // (5d23d29) composition pins on the sibling `:limits` /
14856 // `:behavior` / `:politicas` outer-`Option<&Composite>` arms
14857 // — same "the enumerator gate must route through the
14858 // substrate-primitive typed dispatch" discipline extended onto
14859 // the second of the three M3 mesh-slot axes so the
14860 // [`Caixa::declared_mesh_slots`] enumerator carries the same
14861 // routing invariant on the `:placement` arm as the peer
14862 // `:politicas` arm.
14863 use crate::aplicacao::Placement;
14864 let c = caixa_aplicacao_with_placement(Some(Placement::default()));
14865 let slots = c.declared_mesh_slots();
14866 assert!(
14867 slots.contains(&crate::render::M3_AUTHOR_KEY_PLACEMENT),
14868 "declared_mesh_slots must push M3_AUTHOR_KEY_PLACEMENT \
14869 when `:placement` is Some (even for Placement::default()) \
14870 — the accessor and the enumerator gate must route through \
14871 the same substrate-primitive typed dispatch on the outer \
14872 :placement presence bit (got slots={slots:?})",
14873 );
14874 let c = caixa_aplicacao_with_placement(None);
14875 let slots = c.declared_mesh_slots();
14876 assert!(
14877 !slots.contains(&crate::render::M3_AUTHOR_KEY_PLACEMENT),
14878 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_PLACEMENT \
14879 when `:placement` is None — the author-omitted arm must \
14880 route through the accessor's None-return unchanged (got \
14881 slots={slots:?})",
14882 );
14883 }
14884
14885 #[test]
14886 fn aplicacao_view_placement_arm_folds_through_accessor() {
14887 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:placement`
14888 // Aplicacao-composition seed must fold through
14889 // [`Caixa::placement`], not the raw
14890 // `self.placement.clone().unwrap_or_default()` field-borrow.
14891 // Structurally: a `Caixa { placement: Some(Placement {
14892 // estrategia: Replicated, clusters: ["rio"], .. default }),
14893 // kind: Aplicacao, .. }` must surface a projected
14894 // [`crate::AplicacaoSpec`] whose `placement().estrategia()` +
14895 // `placement().clusters()` byte-equal the outer composite's
14896 // authored values (the fold must project the authored
14897 // composite verbatim), a `Caixa { placement:
14898 // Some(Placement::default()), kind: Aplicacao, .. }` must
14899 // surface an [`crate::AplicacaoSpec`] whose `placement()`
14900 // byte-equals [`crate::aplicacao::Placement::default`] (the
14901 // fold's empty-composite arm collapses to the same default
14902 // the author-omitted arm does), and a `Caixa { placement:
14903 // None, kind: Aplicacao, .. }` must surface an
14904 // [`crate::AplicacaoSpec`] whose `placement()` byte-equals
14905 // [`crate::aplicacao::Placement::default`] (the "author
14906 // omitted the slot entirely" arm folds through the
14907 // `unwrap_or_default` onto the cluster-default). The triad
14908 // jointly pins the accessor + Aplicacao-composition seed
14909 // composition: any future silent detour that had the accessor
14910 // divert the raw slot away from the seed's fold (an operator-
14911 // resolved overlay's default-fold arm silently differing from
14912 // the raw slot's default-fold arm) would silently split the
14913 // build-time distribution-artifact emission gate from the
14914 // caixa-mesh renderer's Aplicacao-view input at the
14915 // composition boundary.
14916 use crate::aplicacao::{Placement, PlacementStrategy};
14917 let c = caixa_aplicacao_with_placement(Some(Placement {
14918 estrategia: PlacementStrategy::Replicated,
14919 clusters: vec!["rio".into()],
14920 affinity: None,
14921 shard_key: None,
14922 }));
14923 let view = c.aplicacao_view().unwrap();
14924 assert_eq!(
14925 view.placement().estrategia(),
14926 PlacementStrategy::Replicated,
14927 "Caixa::aplicacao_view must fold the authored :placement \
14928 :estrategia scalar through the accessor verbatim onto the \
14929 projected AplicacaoSpec — a future silent detour at the \
14930 seed's fold arm would surface here as a projected-scalar \
14931 drift (got {:?})",
14932 view.placement().estrategia(),
14933 );
14934 assert_eq!(
14935 view.placement().clusters(),
14936 &["rio"],
14937 "Caixa::aplicacao_view must fold the authored :placement \
14938 :clusters list through the accessor verbatim onto the \
14939 projected AplicacaoSpec — a future silent detour at the \
14940 seed's fold arm would surface here as a projected-list \
14941 drift (got {:?})",
14942 view.placement().clusters(),
14943 );
14944 let c = caixa_aplicacao_with_placement(Some(Placement::default()));
14945 let view = c.aplicacao_view().unwrap();
14946 assert_eq!(
14947 view.placement(),
14948 &Placement::default(),
14949 "Caixa::aplicacao_view must fold Some(Placement::default()) \
14950 through the accessor onto Placement::default — the empty- \
14951 composite arm collapses to the same default the author- \
14952 omitted arm does (got {:?})",
14953 view.placement(),
14954 );
14955 let c = caixa_aplicacao_with_placement(None);
14956 let view = c.aplicacao_view().unwrap();
14957 assert_eq!(
14958 view.placement(),
14959 &Placement::default(),
14960 "Caixa::aplicacao_view must fold None through the accessor's \
14961 unwrap_or_default onto Placement::default — the author- \
14962 omitted arm must route through the accessor's None-return \
14963 unchanged (got {:?})",
14964 view.placement(),
14965 );
14966 }
14967
14968 #[test]
14969 fn placement_projects_option_ref_by_borrow() {
14970 // The by-borrow pin: [`Caixa::placement`] returns
14971 // `Option<&Placement>` by borrow — the returned reference
14972 // borrows the underlying `Option<Placement>` storage of the
14973 // `:placement` slot and the accessor must not clone the
14974 // backing composite on every call. Peer of the sibling
14975 // `limits_projects_option_ref_by_borrow` (b2bd9d7),
14976 // `behavior_projects_option_ref_by_borrow` (35d8b52), and
14977 // `politicas_projects_option_ref_by_borrow` (5d23d29) by-borrow
14978 // pins on the outer top-level [`Caixa`]
14979 // `Option<&Composite>`-return sub-family — extended here to
14980 // the fourth axis of the same sub-family: the accessor's
14981 // returned reference must borrow from `&self` (the returned
14982 // reference's lifetime is tied to `&self`), and calling the
14983 // accessor twice on the same [`Caixa`] must yield references
14984 // that are pointer-equal (the underlying byte-buffer is the
14985 // storage `Placement`'s allocation, not a fresh copy) as well
14986 // as value-equal (idempotent, no side effects on `&self`).
14987 //
14988 // Pins against a future silent detour that returned an owned
14989 // `Placement` (which would type-check via the `Clone` impl
14990 // but silently clone on every call), a `&Placement` panic-
14991 // return on the `None` arm (which would collapse the load-
14992 // bearing `Option` presence-bit into a runtime panic), or a
14993 // one-arm-only accessor that returned a saturating composite
14994 // on some sentinel input.
14995 use crate::aplicacao::{Placement, PlacementStrategy};
14996 for placement in [
14997 Some(Placement::default()),
14998 Some(Placement {
14999 estrategia: PlacementStrategy::Sharded,
15000 clusters: vec!["rio".into(), "sao-paulo".into()],
15001 affinity: Some("data-locality".into()),
15002 shard_key: Some("$tenantId".into()),
15003 }),
15004 ] {
15005 let c = caixa_aplicacao_with_placement(placement.clone());
15006 let first = c.placement().unwrap();
15007 let second = c.placement().unwrap();
15008 assert_eq!(
15009 first, second,
15010 "Caixa::placement must be idempotent — two successive \
15011 calls on the same &self must return the same \
15012 &Placement",
15013 );
15014 assert!(
15015 std::ptr::eq(first, second),
15016 "Caixa::placement must borrow the underlying \
15017 Option<Placement> storage — two successive calls \
15018 must return references with the same backing pointer \
15019 (a fresh Placement clone would change the pointer on \
15020 every call)",
15021 );
15022 assert_eq!(
15023 Some(first),
15024 placement.as_ref(),
15025 "Caixa::placement must return :placement verbatim by \
15026 borrow — got {first:?}, expected {:?}",
15027 placement.as_ref(),
15028 );
15029 }
15030 let c = caixa_aplicacao_with_placement(None);
15031 assert!(
15032 c.placement().is_none(),
15033 "Caixa::placement must return None when :placement is \
15034 absent — the author-omitted arm must project through the \
15035 accessor's Option::None unchanged",
15036 );
15037 }
15038
15039 // ── Caixa::entrada — outer top-level Option<&Entrada> composite-reference accessor ──
15040
15041 fn caixa_aplicacao_with_entrada(entrada: Option<crate::aplicacao::Entrada>) -> Caixa {
15042 use crate::aplicacao::{Membro, WitContract};
15043 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
15044 c.kind = CaixaKind::Aplicacao;
15045 c.membros = vec![Membro {
15046 caixa: "a".into(),
15047 versao: "^0.1".into(),
15048 }];
15049 c.contratos = vec![WitContract {
15050 de: "a".into(),
15051 para: "a".into(),
15052 wit: "wasi:http/proxy".into(),
15053 endpoint: Some("/x".into()),
15054 subject: None,
15055 slot: None,
15056 }];
15057 c.entrada = entrada;
15058 c
15059 }
15060
15061 #[test]
15062 fn entrada_returns_entrada_option_ref_verbatim_across_permutations() {
15063 // The canonical per-`Caixa` `:entrada` M3 mesh-slot outer-
15064 // composite optional-composite-reference-shape pin:
15065 // [`Caixa::entrada`] must return the `:entrada` typed
15066 // `Option<Entrada>` verbatim as an `Option<&Entrada>`
15067 // reference over the same backing storage the raw
15068 // `self.entrada.as_ref()` field access borrows from,
15069 // byte-equal across every representative fixture in the
15070 // accept-set — the author-omitted `None` shape (the
15071 // "cluster-internal Aplicacao" partition every downstream
15072 // Gateway-API emitter treats as "emit no listener + no
15073 // HTTPRoute"), a bare-`host`/`para` minimum-composite fixture
15074 // (empty `paths` — the resolved-paths fallback the peer
15075 // [`crate::aplicacao::Entrada::resolved_paths`] cascade folds
15076 // onto the substrate catch-all), and a fully-populated
15077 // multi-path-with-non-default-port fixture (the canonical
15078 // shape a public HTTP Aplicacao carries).
15079 //
15080 // Pins against a future silent detour that returned a fresh-
15081 // cloned [`crate::aplicacao::Entrada`] copy (which would
15082 // type-check via the `Clone` impl but silently break every
15083 // downstream caller that relied on the reference sharing the
15084 // composite's backing identity), a reference to an operator-
15085 // resolved overlay (the future per-cluster
15086 // `:entrada-overrides` slot — its resolution must land at
15087 // exactly this accessor body, not silently divert the raw
15088 // slot away from the peer [`Caixa::declared_mesh_slots`]
15089 // enumerator's presence probe), or an axis-shuffled projection
15090 // (a future detour that swapped `host` and `para` through the
15091 // accessor would silently split the paired
15092 // [`Caixa::aplicacao_view`] seed's forward input from the
15093 // sibling M3 gateway-artifact emitter's projection input).
15094 //
15095 // Fifth and final outer top-level [`Caixa`]
15096 // `Option<&Composite>`-return composite-reference accessor pin
15097 // on the substrate primitive — peer of the sibling
15098 // `limits_returns_limits_option_ref_verbatim_across_permutations`
15099 // (b2bd9d7),
15100 // `behavior_returns_behavior_option_ref_verbatim_across_permutations`
15101 // (35d8b52),
15102 // `politicas_returns_politicas_option_ref_verbatim_across_permutations`
15103 // (5d23d29), and
15104 // `placement_returns_placement_option_ref_verbatim_across_permutations`
15105 // (4fb8074) opening tetrad pins on the outer top-level
15106 // [`Caixa`] `Option<&Composite>`-return sub-family — extended
15107 // here to the third and final M3 mesh-slot axis so the closed
15108 // outer `Option<&Composite>` sub-family carries the same
15109 // "byte-equal, borrow-shared, presence-bit-preserved" outer-
15110 // accessor discipline across all five arms.
15111 use crate::aplicacao::Entrada;
15112 let fixtures: Vec<Option<Entrada>> = vec![
15113 None,
15114 Some(Entrada {
15115 host: "checkout.quero.cloud".into(),
15116 para: "gateway".into(),
15117 paths: Vec::new(),
15118 port: crate::DEFAULT_SERVICO_PORT,
15119 }),
15120 Some(Entrada {
15121 host: "api.pleme.io".into(),
15122 para: "public-api".into(),
15123 paths: vec!["/v1".into(), "/v2".into()],
15124 port: 8080,
15125 }),
15126 ];
15127 for entrada in fixtures {
15128 let c = caixa_aplicacao_with_entrada(entrada.clone());
15129 assert_eq!(
15130 c.entrada(),
15131 entrada.as_ref(),
15132 "Caixa::entrada must return :entrada verbatim (got \
15133 {:?}, expected {:?})",
15134 c.entrada(),
15135 entrada.as_ref(),
15136 );
15137 match (c.entrada(), c.entrada.as_ref()) {
15138 (Some(a), Some(b)) => assert!(
15139 std::ptr::eq(a, b),
15140 "Caixa::entrada accessor and self.entrada.as_ref() \
15141 field access must borrow the same backing storage \
15142 — the accessor is the substrate-primitive typed \
15143 dispatch every downstream Aplicacao-external- \
15144 gateway composite consumer must route through, and \
15145 a reference-identity split would silently break \
15146 every consumer that relied on the borrow sharing \
15147 the composite's storage",
15148 ),
15149 (None, None) => {}
15150 _ => panic!(
15151 "Caixa::entrada presence bit must byte-equal \
15152 self.entrada.is_some() — a presence-bit drift \
15153 would silently split the paired \
15154 Caixa::aplicacao_view Aplicacao-composition seed's \
15155 traversal head from the peer \
15156 Caixa::declared_mesh_slots M3 declared-slot \
15157 enumerator's presence probe",
15158 ),
15159 }
15160 assert_eq!(
15161 c.entrada().is_some(),
15162 c.entrada.is_some(),
15163 "Caixa::entrada().is_some() must byte-equal \
15164 self.entrada.is_some() — a presence-bit drift would \
15165 silently split every downstream Option<&Entrada> \
15166 consumer's partition on the cluster-internal arm",
15167 );
15168 }
15169 }
15170
15171 #[test]
15172 fn declared_mesh_slots_entrada_arm_routes_through_accessor() {
15173 // Composition pin: [`Caixa::declared_mesh_slots`]'s `:entrada`
15174 // presence-probe arm must key off [`Caixa::entrada`], not the
15175 // raw `self.entrada.is_some()` field-probe. Structurally: a
15176 // `Caixa { entrada: Some(Entrada { host: "...", para: "...",
15177 // paths: [], port: DEFAULT_SERVICO_PORT }), .. }` must push
15178 // `M3_AUTHOR_KEY_ENTRADA` onto the declared-slot list (the
15179 // presence bit is `Some`, so the M3 kind-coherence gate must
15180 // surface the slot as "declared" even when every per-axis
15181 // scalar defers to the substrate catch-all / default port),
15182 // and a `Caixa { entrada: None, .. }` must NOT push the label
15183 // (the "author omitted the slot entirely" partition). The pair
15184 // jointly pins the accessor + declared-slot enumerator
15185 // composition: any future silent detour that had the accessor
15186 // collapse `Some(Entrada { paths: [], .. })` to `None` (a
15187 // `.filter(|e| !e.paths.is_empty())` projection) would silently
15188 // absorb the "declared but empty-paths" arm at the accessor
15189 // boundary and the
15190 // [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
15191 // coherence gate would silently accept a struct-literal
15192 // `Caixa` carrying the drift.
15193 //
15194 // Peer of the sibling
15195 // `declared_servico_slots_limits_arm_routes_through_accessor`
15196 // (b2bd9d7),
15197 // `declared_servico_slots_behavior_arm_routes_through_accessor`
15198 // (35d8b52),
15199 // `declared_mesh_slots_politicas_arm_routes_through_accessor`
15200 // (5d23d29), and
15201 // `declared_mesh_slots_placement_arm_routes_through_accessor`
15202 // (4fb8074) composition pins on the sibling `:limits` /
15203 // `:behavior` / `:politicas` / `:placement` outer-
15204 // `Option<&Composite>` arms — same "the enumerator gate must
15205 // route through the substrate-primitive typed dispatch"
15206 // discipline extended onto the third and final M3 mesh-slot
15207 // axis so the [`Caixa::declared_mesh_slots`] enumerator now
15208 // carries the routing invariant on every M3 mesh-slot arm.
15209 use crate::aplicacao::Entrada;
15210 let c = caixa_aplicacao_with_entrada(Some(Entrada {
15211 host: "checkout.quero.cloud".into(),
15212 para: "gateway".into(),
15213 paths: Vec::new(),
15214 port: crate::DEFAULT_SERVICO_PORT,
15215 }));
15216 let slots = c.declared_mesh_slots();
15217 assert!(
15218 slots.contains(&crate::render::M3_AUTHOR_KEY_ENTRADA),
15219 "declared_mesh_slots must push M3_AUTHOR_KEY_ENTRADA when \
15220 `:entrada` is Some (even for empty-paths / default-port) \
15221 — the accessor and the enumerator gate must route through \
15222 the same substrate-primitive typed dispatch on the outer \
15223 :entrada presence bit (got slots={slots:?})",
15224 );
15225 let c = caixa_aplicacao_with_entrada(None);
15226 let slots = c.declared_mesh_slots();
15227 assert!(
15228 !slots.contains(&crate::render::M3_AUTHOR_KEY_ENTRADA),
15229 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_ENTRADA \
15230 when `:entrada` is None — the author-omitted arm must \
15231 route through the accessor's None-return unchanged (got \
15232 slots={slots:?})",
15233 );
15234 }
15235
15236 #[test]
15237 fn aplicacao_view_entrada_arm_folds_through_accessor() {
15238 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:entrada`
15239 // Aplicacao-composition seed must fold through
15240 // [`Caixa::entrada`], not the raw `self.entrada.clone()` field-
15241 // borrow. Structurally: a `Caixa { entrada: Some(Entrada {
15242 // host: "api.pleme.io", para: "public-api", paths: ["/v1"],
15243 // port: 8080 }), kind: Aplicacao, .. }` must surface a projected
15244 // [`crate::AplicacaoSpec`] whose `entrada().unwrap()` byte-
15245 // equals the outer composite's authored value (the fold must
15246 // project the authored composite verbatim), and a `Caixa {
15247 // entrada: None, kind: Aplicacao, .. }` must surface an
15248 // [`crate::AplicacaoSpec`] whose `entrada()` is `None` (the
15249 // "author omitted the slot entirely" arm folds through the
15250 // accessor's `Option::cloned` onto the same `None` presence
15251 // bit — unlike the peer `:politicas` / `:placement` arms
15252 // `:entrada` has no cluster-default fold, the omitted arm
15253 // stays omitted). The pair jointly pins the accessor +
15254 // Aplicacao-composition seed composition: any future silent
15255 // detour that had the accessor divert the raw slot away from
15256 // the seed's fold (an operator-resolved overlay's forward arm
15257 // silently differing from the raw slot's forward arm) would
15258 // silently split the build-time gateway-artifact emission gate
15259 // from the caixa-mesh renderer's Aplicacao-view input at the
15260 // composition boundary.
15261 use crate::aplicacao::Entrada;
15262 let authored = Entrada {
15263 host: "api.pleme.io".into(),
15264 para: "public-api".into(),
15265 paths: vec!["/v1".into()],
15266 port: 8080,
15267 };
15268 let c = caixa_aplicacao_with_entrada(Some(authored.clone()));
15269 let view = c.aplicacao_view().unwrap();
15270 assert_eq!(
15271 view.entrada(),
15272 Some(&authored),
15273 "Caixa::aplicacao_view must fold the authored :entrada \
15274 composite through the accessor verbatim onto the \
15275 projected AplicacaoSpec — a future silent detour at the \
15276 seed's fold arm would surface here as a projected- \
15277 composite drift (got {:?})",
15278 view.entrada(),
15279 );
15280 let c = caixa_aplicacao_with_entrada(None);
15281 let view = c.aplicacao_view().unwrap();
15282 assert!(
15283 view.entrada().is_none(),
15284 "Caixa::aplicacao_view must fold None through the \
15285 accessor's Option::cloned onto None — the author- \
15286 omitted arm must route through the accessor's None-return \
15287 unchanged (got {:?})",
15288 view.entrada(),
15289 );
15290 }
15291
15292 #[test]
15293 fn entrada_projects_option_ref_by_borrow() {
15294 // The by-borrow pin: [`Caixa::entrada`] returns
15295 // `Option<&Entrada>` by borrow — the returned reference
15296 // borrows the underlying `Option<Entrada>` storage of the
15297 // `:entrada` slot and the accessor must not clone the backing
15298 // composite on every call. Peer of the sibling
15299 // `limits_projects_option_ref_by_borrow` (b2bd9d7),
15300 // `behavior_projects_option_ref_by_borrow` (35d8b52),
15301 // `politicas_projects_option_ref_by_borrow` (5d23d29), and
15302 // `placement_projects_option_ref_by_borrow` (4fb8074) by-
15303 // borrow pins on the outer top-level [`Caixa`]
15304 // `Option<&Composite>`-return sub-family — extended here to
15305 // the fifth and final axis of the same sub-family, closing
15306 // the discipline: the accessor's returned reference must
15307 // borrow from `&self` (the returned reference's lifetime is
15308 // tied to `&self`), and calling the accessor twice on the
15309 // same [`Caixa`] must yield references that are pointer-equal
15310 // (the underlying byte-buffer is the storage `Entrada`'s
15311 // allocation, not a fresh copy) as well as value-equal
15312 // (idempotent, no side effects on `&self`).
15313 //
15314 // Pins against a future silent detour that returned an owned
15315 // `Entrada` (which would type-check via the `Clone` impl but
15316 // silently clone on every call), a `&Entrada` panic-return on
15317 // the `None` arm (which would collapse the load-bearing
15318 // `Option` presence-bit into a runtime panic), or a one-arm-
15319 // only accessor that returned a saturating composite on some
15320 // sentinel input.
15321 use crate::aplicacao::Entrada;
15322 for entrada in [
15323 Some(Entrada {
15324 host: "checkout.quero.cloud".into(),
15325 para: "gateway".into(),
15326 paths: Vec::new(),
15327 port: crate::DEFAULT_SERVICO_PORT,
15328 }),
15329 Some(Entrada {
15330 host: "api.pleme.io".into(),
15331 para: "public-api".into(),
15332 paths: vec!["/v1".into(), "/v2".into()],
15333 port: 8080,
15334 }),
15335 ] {
15336 let c = caixa_aplicacao_with_entrada(entrada.clone());
15337 let first = c.entrada().unwrap();
15338 let second = c.entrada().unwrap();
15339 assert_eq!(
15340 first, second,
15341 "Caixa::entrada must be idempotent — two successive \
15342 calls on the same &self must return the same &Entrada",
15343 );
15344 assert!(
15345 std::ptr::eq(first, second),
15346 "Caixa::entrada must borrow the underlying \
15347 Option<Entrada> storage — two successive calls must \
15348 return references with the same backing pointer (a \
15349 fresh Entrada clone would change the pointer on every \
15350 call)",
15351 );
15352 assert_eq!(
15353 Some(first),
15354 entrada.as_ref(),
15355 "Caixa::entrada must return :entrada verbatim by \
15356 borrow — got {first:?}, expected {:?}",
15357 entrada.as_ref(),
15358 );
15359 }
15360 let c = caixa_aplicacao_with_entrada(None);
15361 assert!(
15362 c.entrada().is_none(),
15363 "Caixa::entrada must return None when :entrada is absent \
15364 — the author-omitted arm must project through the \
15365 accessor's Option::None unchanged",
15366 );
15367 }
15368
15369 // ── Caixa::estrategia — outer top-level Option<RestartStrategy> flat-spread supervisor-tree accessor ──
15370
15371 fn caixa_with_estrategia(estrategia: Option<crate::supervisor::RestartStrategy>) -> Caixa {
15372 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
15373 c.estrategia = estrategia;
15374 c
15375 }
15376
15377 #[test]
15378 fn estrategia_returns_estrategia_option_verbatim_across_permutations() {
15379 // The canonical per-`Caixa` `:estrategia` M2 supervisor-tree-slot
15380 // flat-spread `Option<RestartStrategy>`-return `Copy`-composite-
15381 // enum-arm scalar shape pin: [`Caixa::estrategia`] must return
15382 // the `:estrategia` typed `Option<crate::supervisor::RestartStrategy>`
15383 // verbatim as an `Option<RestartStrategy>` `Copy`-projected value
15384 // over the same discriminant the raw `self.estrategia` field
15385 // access carries, byte-equal across every representative fixture
15386 // in the accept-set — the author-omitted `None` shape (the
15387 // "defer to [`RestartStrategy::default`] through the
15388 // [`Self::supervisor_view`] `unwrap_or_default()` fold" partition
15389 // every non-`Supervisor`-kind `defcaixa` carries by
15390 // `#[serde(default)]`), and each of the four closed-set variants
15391 // [`RestartStrategy::OneForOne`] / [`RestartStrategy::OneForAll`]
15392 // / [`RestartStrategy::RestForOne`] /
15393 // [`RestartStrategy::SimpleOneForOne`] the author-declared arm
15394 // partitions on.
15395 //
15396 // Pins against a future silent detour that re-derived the
15397 // strategy from a peer axis (an accidental fallback to
15398 // `if children.is_empty() { SimpleOneForOne } else { OneForOne }`
15399 // collapse that read the outer `:children` list-length axis into
15400 // the strategy discriminator at the accessor boundary), a
15401 // stale-derive detour that substituted [`RestartStrategy::default`]
15402 // when the outer `Option` held `None` (which would silently
15403 // collapse the load-bearing "author explicitly declared
15404 // `:estrategia OneForOne`" vs "author omitted the slot and
15405 // inherited the default" partition the [`Self::declared_supervisor_slots`]
15406 // presence-probe reads — the enumerator gate would still push
15407 // `SUPERVISOR_AUTHOR_KEY_ESTRATEGIA` on the omitted arm, silently
15408 // splitting the paired [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
15409 // kind-coherence gate's traversal head from the
15410 // [`Self::supervisor_view`] `unwrap_or_default()` fold's
15411 // composition head), a reference to an operator-resolved overlay
15412 // (the future per-cluster `:estrategia-overrides` slot — its
15413 // resolution must land at exactly this accessor body, not
15414 // silently divert the raw slot away from a second consumer), or
15415 // an axis-remap projection (a future detour that mapped
15416 // `OneForAll` through the accessor onto `OneForOne` would
15417 // silently split every downstream sibling-restart-strategy
15418 // consumer's per-arm fan-out).
15419 //
15420 // First outer top-level [`Caixa`] `Option<Copy>`-return
15421 // supervisor-tree-slot flat-spread accessor pin on the substrate
15422 // primitive — opens the outer-`Caixa` `Option<Copy>` flat-spread
15423 // projection pattern the sibling per-`Caixa` `:max-restarts` /
15424 // `:restart-window` future outer-scalar pins fold on. Peer of
15425 // the inner-altitude
15426 // `supervisor_spec_estrategia_returns_estrategia_verbatim_across_permutations`
15427 // (eafb619) pin on the post-composition [`SupervisorSpec`]
15428 // altitude — same "the substrate-primitive accessor must byte-
15429 // equal the raw field access verbatim across every author-
15430 // declared value" discipline extended onto the pre-composition
15431 // outer author-surface [`Caixa`] altitude. Peer of the closed
15432 // outer-`Caixa` `Option<&Composite>` composite-reference family
15433 // the sibling `limits` / `behavior` / `politicas` / `placement` /
15434 // `entrada`
15435 // `..._returns_..._option_ref_verbatim_across_permutations` pins
15436 // already carry on the outer `Option<&Composite>` altitude.
15437 use crate::supervisor::RestartStrategy;
15438 let fixtures: Vec<Option<RestartStrategy>> = vec![
15439 None,
15440 Some(RestartStrategy::OneForOne),
15441 Some(RestartStrategy::OneForAll),
15442 Some(RestartStrategy::RestForOne),
15443 Some(RestartStrategy::SimpleOneForOne),
15444 ];
15445 for estrategia in fixtures {
15446 let c = caixa_with_estrategia(estrategia);
15447 assert_eq!(
15448 c.estrategia(),
15449 estrategia,
15450 "Caixa::estrategia must return :estrategia verbatim (got \
15451 {:?}, expected {:?})",
15452 c.estrategia(),
15453 estrategia,
15454 );
15455 assert_eq!(
15456 c.estrategia(),
15457 c.estrategia,
15458 "Caixa::estrategia accessor and self.estrategia field \
15459 access must byte-equal — the accessor is the substrate-\
15460 primitive typed dispatch every downstream supervisor-\
15461 tree flat-spread consumer must route through, and a \
15462 discriminant split would silently break every consumer \
15463 that relied on the accessor sharing the field's own \
15464 Option<Copy> shape",
15465 );
15466 assert_eq!(
15467 c.estrategia().is_some(),
15468 c.estrategia.is_some(),
15469 "Caixa::estrategia().is_some() must byte-equal \
15470 self.estrategia.is_some() — a presence-bit drift would \
15471 silently split the paired Caixa::declared_supervisor_slots \
15472 presence-probe arm from the Caixa::supervisor_view \
15473 unwrap_or_default() fold's composition input",
15474 );
15475 }
15476 }
15477
15478 #[test]
15479 fn declared_supervisor_slots_estrategia_arm_routes_through_accessor() {
15480 // Composition pin: [`Caixa::declared_supervisor_slots`]'s
15481 // `:estrategia` presence-probe arm must key off
15482 // [`Caixa::estrategia`], not the raw `self.estrategia.is_some()`
15483 // field-probe. Structurally: every `Caixa { estrategia:
15484 // Some(RestartStrategy::_), .. }` variant must push
15485 // `SUPERVISOR_AUTHOR_KEY_ESTRATEGIA` onto the declared-slot list
15486 // (the presence bit is `Some` for every closed-set variant, so
15487 // the M2 supervisor-tree kind-coherence gate must surface the
15488 // slot as "declared" regardless of which variant the author
15489 // picked), and a `Caixa { estrategia: None, .. }` must NOT push
15490 // the label (the "author omitted the slot entirely, deferring
15491 // to [`RestartStrategy::default`] through the supervisor_view
15492 // fold" partition). The pair jointly pins the accessor +
15493 // declared-slot enumerator composition: any future silent detour
15494 // that had the accessor collapse `Some(RestartStrategy::default())`
15495 // to `None` (a `.filter(|e| *e != RestartStrategy::default())`
15496 // projection) would silently absorb the "declared but default-
15497 // valued" arm at the accessor boundary and the
15498 // [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] kind-
15499 // coherence gate would silently accept a struct-literal `Caixa`
15500 // carrying the drift.
15501 //
15502 // Peer of the sibling per-`Caixa`
15503 // `declared_servico_slots_limits_arm_routes_through_accessor`
15504 // (b2bd9d7) accessor-composition pin on the sibling outer-`Caixa`
15505 // `Option<&LimitsSpec>` composition axis — same "the enumerator
15506 // gate must route through the substrate-primitive typed
15507 // dispatch" discipline extended onto the flat-spread M2
15508 // supervisor-tree `Option<RestartStrategy>`-composition surface,
15509 // opening the outer-`Caixa` supervisor-tree-slot arm of the
15510 // composition-pin family.
15511 use crate::supervisor::RestartStrategy;
15512 for estrategia in [
15513 RestartStrategy::OneForOne,
15514 RestartStrategy::OneForAll,
15515 RestartStrategy::RestForOne,
15516 RestartStrategy::SimpleOneForOne,
15517 ] {
15518 let c = caixa_with_estrategia(Some(estrategia));
15519 let slots = c.declared_supervisor_slots();
15520 assert!(
15521 slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA),
15522 "declared_supervisor_slots must push \
15523 SUPERVISOR_AUTHOR_KEY_ESTRATEGIA when `:estrategia` is \
15524 Some({estrategia:?}) — the accessor and the enumerator \
15525 gate must route through the same substrate-primitive \
15526 typed dispatch on the outer :estrategia presence bit \
15527 (got slots={slots:?})",
15528 );
15529 }
15530 let c = caixa_with_estrategia(None);
15531 let slots = c.declared_supervisor_slots();
15532 assert!(
15533 !slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA),
15534 "declared_supervisor_slots must NOT push \
15535 SUPERVISOR_AUTHOR_KEY_ESTRATEGIA when `:estrategia` is None \
15536 — the author-omitted arm must route through the accessor's \
15537 None-return unchanged (got slots={slots:?})",
15538 );
15539 }
15540
15541 #[test]
15542 fn supervisor_view_estrategia_arm_routes_through_accessor() {
15543 // Composition pin: [`Caixa::supervisor_view`]'s per-`:estrategia`
15544 // [`SupervisorSpec`] construction arm must key off
15545 // [`Caixa::estrategia`]'s `unwrap_or_default()` fold, not the raw
15546 // `self.estrategia.unwrap_or_default()` field-fold. Structurally:
15547 // for every `:kind Supervisor` `Caixa` carrying an author-
15548 // declared `Some(RestartStrategy::_)` variant, the composed
15549 // [`SupervisorSpec`]'s `.estrategia` field must byte-equal the
15550 // outer accessor's declared variant unchanged; and for a
15551 // `:kind Supervisor` `Caixa` carrying `None`, the composed
15552 // [`SupervisorSpec`]'s `.estrategia` field must byte-equal
15553 // [`RestartStrategy::default`] (the [`RestartStrategy::OneForOne`]
15554 // arm the flat-spread `unwrap_or_default()` fold projects to on
15555 // the author-omitted arm — this is the *composition* between the
15556 // outer `Option<RestartStrategy>` accessor's presence-bit
15557 // surface and the inner post-composition non-`Option`
15558 // [`SupervisorSpec::estrategia`] altitude). The pair jointly
15559 // pins the accessor + supervisor_view composition: any future
15560 // silent detour that had the accessor promote `None` to
15561 // `Some(RestartStrategy::default())` (a `.or_else(|| Some(RestartStrategy::default()))`
15562 // projection) would silently collapse the two arms into one at
15563 // the accessor boundary and the [`Self::declared_supervisor_slots`]
15564 // presence probe would silently drift from the composition site.
15565 //
15566 // Peer of the sibling M2 supervisor-slot post-composition
15567 // `validate_reads_through_lifted_estrategia_accessor` (eafb619)
15568 // pin on the [`SupervisorSpec::validate`] altitude — this pin
15569 // extends that inner-altitude accessor-routing discipline onto
15570 // the pre-composition outer author-surface [`Caixa`] altitude,
15571 // pinning the composition edge between the flat-spread outer
15572 // `Option<RestartStrategy>` and the composed [`SupervisorSpec`]
15573 // `RestartStrategy` axes.
15574 use crate::CaixaKind;
15575 use crate::supervisor::{ChildSpec, RestartPolicy, RestartStrategy};
15576 for estrategia in [
15577 RestartStrategy::OneForOne,
15578 RestartStrategy::OneForAll,
15579 RestartStrategy::RestForOne,
15580 RestartStrategy::SimpleOneForOne,
15581 ] {
15582 let mut c = caixa_with_estrategia(Some(estrategia));
15583 c.kind = CaixaKind::Supervisor;
15584 // Route the `SimpleOneForOne ↔ non-SimpleOneForOne` fixture-
15585 // shape partition through the [`gen_platform::IsVariant`]
15586 // derive-generated
15587 // [`RestartStrategy::is_simple_one_for_one`] predicate rather
15588 // than the raw `matches!(estrategia, RestartStrategy::
15589 // SimpleOneForOne)` open-coded pattern-match — same closed-
15590 // set-typed-enum arm-discriminator dispatch discipline the
15591 // sibling [`crate::upgrade::UpgradeInstruction::is_restart`]
15592 // convergence (915a934) extended onto its two paired positive
15593 // / negated `matches!` sites and the peer
15594 // [`crate::aplicacao::PlacementStrategy`] `IsVariant`-derived
15595 // predicate convergence (766ec63) extended onto the M3 mesh-
15596 // slot per-`:placement` distribution-strategy discriminator
15597 // axis. See the sibling `supervisor::tests::
15598 // round_trip_all_strategies` and
15599 // `supervisor::tests::supervisor_spec_estrategia_returns_estrategia_verbatim_across_permutations`
15600 // fixtures — the three sites (all test-only,
15601 // acknowledged in 915a934's Prior-commits footnote as the
15602 // outstanding follow-up) now consult one typed dispatch on
15603 // the substrate primitive.
15604 c.children = if estrategia.is_simple_one_for_one() {
15605 Vec::new()
15606 } else {
15607 vec![ChildSpec {
15608 caixa: "worker".into(),
15609 versao: "^0.1".into(),
15610 restart: RestartPolicy::Permanent,
15611 }]
15612 };
15613 let view = c.supervisor_view().expect(
15614 "supervisor_view must materialize a SupervisorSpec for a \
15615 :kind Supervisor Caixa carrying a Some(:estrategia) slot",
15616 );
15617 assert_eq!(
15618 view.estrategia(),
15619 c.estrategia().unwrap(),
15620 "supervisor_view must carry the outer Caixa::estrategia() \
15621 declared variant onto the composed SupervisorSpec.estrategia \
15622 field verbatim on the Some arm (got {:?}, expected {:?})",
15623 view.estrategia(),
15624 c.estrategia().unwrap(),
15625 );
15626 }
15627 // The author-omitted arm: outer `None` → composed
15628 // `RestartStrategy::default()` through the flat-spread
15629 // `unwrap_or_default()` fold.
15630 let mut c = caixa_with_estrategia(None);
15631 c.kind = CaixaKind::Supervisor;
15632 // Populate children so the sibling supervisor slots are coherent
15633 // for the [`Self::supervisor_view`] projection; the `:estrategia`
15634 // arm still defers to [`RestartStrategy::default`] on the
15635 // author-omitted arm even when the sibling slots carry values.
15636 c.children = vec![ChildSpec {
15637 caixa: "worker".into(),
15638 versao: "^0.1".into(),
15639 restart: RestartPolicy::Permanent,
15640 }];
15641 let view = c.supervisor_view().expect(
15642 "supervisor_view must materialize a SupervisorSpec for a \
15643 :kind Supervisor Caixa carrying a None `:estrategia` slot",
15644 );
15645 assert_eq!(
15646 view.estrategia(),
15647 RestartStrategy::default(),
15648 "supervisor_view must project the outer Caixa::estrategia() \
15649 None arm onto RestartStrategy::default() through the flat-\
15650 spread unwrap_or_default() fold (got {:?}, expected {:?})",
15651 view.estrategia(),
15652 RestartStrategy::default(),
15653 );
15654 assert!(
15655 c.estrategia().is_none(),
15656 "Caixa::estrategia() must remain None on the author-omitted \
15657 arm — the supervisor_view fold must not mutate the outer \
15658 flat-spread presence bit",
15659 );
15660 }
15661
15662 #[test]
15663 fn estrategia_projects_option_by_copy() {
15664 // The by-`Copy` pin: [`Caixa::estrategia`] returns
15665 // `Option<RestartStrategy>` by value (`RestartStrategy: Copy`) —
15666 // the accessor does not borrow `&self` past the call (no
15667 // lifetime on the return type), and calling the accessor twice
15668 // on the same [`Caixa`] must yield discriminant-equal values
15669 // (idempotent, no side effects on `&self`). Peer of the sibling
15670 // outer-`Caixa` `Option<&Composite>` by-borrow
15671 // `limits_projects_option_ref_by_borrow` (b2bd9d7) /
15672 // `behavior_projects_option_ref_by_borrow` (35d8b52) /
15673 // `politicas_projects_option_ref_by_borrow` (5d23d29) /
15674 // `placement_projects_option_ref_by_borrow` (4fb8074) /
15675 // `entrada_projects_option_ref_by_borrow` (e4128e4) by-borrow
15676 // pins on the outer-`Caixa` `Option<&Composite>`-return axes —
15677 // extended here to the outer-`Caixa` `Option<Copy>`-return
15678 // flat-spread axis. The `Copy` discipline replaces the pointer-
15679 // equality claim the by-borrow siblings pin (a fresh `Copy` of a
15680 // `Copy` discriminant is definitionally the same discriminant, so
15681 // the axis reduces to discriminant equality).
15682 //
15683 // Pins against a future silent detour that returned a fresh
15684 // `Option<&RestartStrategy>` (which would type-check but silently
15685 // introduce a borrow of `&self` past the call, collapsing the
15686 // load-bearing "no lifetime on the return type" `Copy` projection
15687 // the flat-spread axis's `Option<Copy>` shape carries), a stale-
15688 // read side effect that flipped the outer discriminant on
15689 // successive calls, or an axis-remap projection that returned a
15690 // different variant than the field storage.
15691 use crate::supervisor::RestartStrategy;
15692 for estrategia in [
15693 Some(RestartStrategy::OneForOne),
15694 Some(RestartStrategy::OneForAll),
15695 Some(RestartStrategy::RestForOne),
15696 Some(RestartStrategy::SimpleOneForOne),
15697 ] {
15698 let c = caixa_with_estrategia(estrategia);
15699 let first = c.estrategia();
15700 let second = c.estrategia();
15701 assert_eq!(
15702 first, second,
15703 "Caixa::estrategia must be idempotent — two successive \
15704 calls on the same &self must return the same \
15705 Option<RestartStrategy>",
15706 );
15707 assert_eq!(
15708 first, estrategia,
15709 "Caixa::estrategia must return :estrategia verbatim by \
15710 Copy — got {first:?}, expected {estrategia:?}",
15711 );
15712 }
15713 let c = caixa_with_estrategia(None);
15714 assert!(
15715 c.estrategia().is_none(),
15716 "Caixa::estrategia must return None when :estrategia is \
15717 absent — the author-omitted arm must project through the \
15718 accessor's Option::None unchanged",
15719 );
15720 }
15721
15722 // ── Caixa::max_restarts / Caixa::restart_window —
15723 // outer top-level M2 supervisor-tree-slot flat-spread accessors
15724 // (Option<u32> / Option<&str>) folding on the ed04d3c
15725 // Caixa::estrategia Option<Copy> sub-family ─────────────────────
15726
15727 fn caixa_with_max_restarts(max_restarts: Option<u32>) -> Caixa {
15728 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
15729 c.max_restarts = max_restarts;
15730 c
15731 }
15732
15733 fn caixa_supervisor_with_max_restarts_and_window(
15734 max_restarts: Option<u32>,
15735 restart_window: Option<&str>,
15736 ) -> Caixa {
15737 use crate::CaixaKind;
15738 use crate::supervisor::{ChildSpec, RestartPolicy};
15739 let mut c = Caixa::from_lisp(&Caixa::template("root")).unwrap();
15740 c.kind = CaixaKind::Supervisor;
15741 c.max_restarts = max_restarts;
15742 c.restart_window = restart_window.map(str::to_string);
15743 c.children = vec![ChildSpec {
15744 caixa: "worker".into(),
15745 versao: "^0.1".into(),
15746 restart: RestartPolicy::Permanent,
15747 }];
15748 c
15749 }
15750
15751 #[test]
15752 fn max_restarts_returns_max_restarts_option_verbatim_across_permutations() {
15753 // Value-shape pin: [`Caixa::max_restarts`] returns the
15754 // `:max-restarts` typed `Option<u32>` verbatim, `Copy`-projected
15755 // from the typed slot's own storage, byte-equal across the
15756 // author-omitted `None` arm (the "defer to the
15757 // [`Self::supervisor_view`] `unwrap_or(5)` OTP-canonical
15758 // `{intensity, 5, 60}` default" partition every
15759 // non-`Supervisor`-kind caixa carries by `#[serde(default)]`)
15760 // and each of the representative fixtures in the accept-set —
15761 // `0` (the zero-floor arm the peer
15762 // [`crate::supervisor::SupervisorSpec::validate`]
15763 // [`crate::SupervisorError::ZeroMaxRestarts`] gate refuses on
15764 // the post-composition altitude — the accessor must ship the
15765 // raw slot verbatim so struct-literal fixtures continue to
15766 // expose the zero at the accessor boundary), the OTP-canonical
15767 // `5` default (`{intensity, 5, 60}` worker-supervisor from
15768 // Learn You Some Erlang), `1000` (the
15769 // [`SUPERVISOR_MAX_RESTARTS_MAX`] cap the peer post-composition
15770 // upper-bound gate accepts on the boundary), `u32::MAX` (a
15771 // past-the-cap sentinel that the substrate-primitive accessor
15772 // must still ship verbatim). Second outer top-level
15773 // [`Caixa`] `Option<Copy>`-return supervisor-tree flat-spread
15774 // pin — folds on the sibling
15775 // `estrategia_returns_estrategia_option_verbatim_across_permutations`
15776 // (ed04d3c) pin's `Option<Copy>` shape, extending the sub-family
15777 // onto the sibling `Option<u32>` restart-budget-count arm.
15778 let fixtures: Vec<Option<u32>> = vec![None, Some(0), Some(5), Some(1000), Some(u32::MAX)];
15779 for max_restarts in fixtures {
15780 let c = caixa_with_max_restarts(max_restarts);
15781 assert_eq!(
15782 c.max_restarts(),
15783 max_restarts,
15784 "Caixa::max_restarts must return :max-restarts verbatim \
15785 (got {:?}, expected {max_restarts:?})",
15786 c.max_restarts(),
15787 );
15788 assert_eq!(
15789 c.max_restarts(),
15790 c.max_restarts,
15791 "Caixa::max_restarts accessor and self.max_restarts \
15792 field access must byte-equal — a presence-bit or count \
15793 drift would silently split the paired \
15794 Caixa::declared_supervisor_slots presence-probe arm \
15795 from the Caixa::supervisor_view unwrap_or(5) fold's \
15796 composition input",
15797 );
15798 }
15799 }
15800
15801 #[test]
15802 fn max_restarts_projects_option_by_copy() {
15803 // The by-`Copy` pin: [`Caixa::max_restarts`] returns
15804 // `Option<u32>` by value (`u32: Copy`) — the accessor does not
15805 // borrow `&self` past the call (no lifetime on the return type),
15806 // and calling the accessor twice on the same [`Caixa`] must
15807 // yield equal values (idempotent, no side effects). Peer of the
15808 // sibling `estrategia_projects_option_by_copy` (ed04d3c) pin on
15809 // the outer-`Caixa` `Option<Copy>`-return flat-spread axis.
15810 for max_restarts in [Some(0u32), Some(5), Some(1000), Some(u32::MAX), None] {
15811 let c = caixa_with_max_restarts(max_restarts);
15812 let first = c.max_restarts();
15813 let second = c.max_restarts();
15814 assert_eq!(
15815 first, second,
15816 "Caixa::max_restarts must be idempotent — two successive \
15817 calls on the same &self must return the same Option<u32>",
15818 );
15819 assert_eq!(
15820 first, max_restarts,
15821 "Caixa::max_restarts must return :max-restarts verbatim \
15822 by Copy — got {first:?}, expected {max_restarts:?}",
15823 );
15824 }
15825 }
15826
15827 #[test]
15828 fn declared_supervisor_slots_max_restarts_arm_routes_through_accessor() {
15829 // Composition pin: [`Caixa::declared_supervisor_slots`]'s
15830 // `:max-restarts` presence-probe arm must key off
15831 // [`Caixa::max_restarts`], not the raw
15832 // `self.max_restarts.is_some()` field-probe. Structurally: every
15833 // `Caixa { max_restarts: Some(_), .. }` variant must push
15834 // `SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS` onto the declared-slot
15835 // list (the presence bit is `Some` for every representative
15836 // count, so the M2 kind-coherence gate must surface the slot as
15837 // "declared"), and a `Caixa { max_restarts: None, .. }` must
15838 // NOT push the label. Peer of the sibling
15839 // `declared_supervisor_slots_estrategia_arm_routes_through_accessor`
15840 // (ed04d3c) composition pin — same routing-through-accessor
15841 // discipline extended onto the sibling flat-spread `Option<u32>`
15842 // arm.
15843 for max_restarts in [0u32, 5, 1000, u32::MAX] {
15844 let c = caixa_with_max_restarts(Some(max_restarts));
15845 let slots = c.declared_supervisor_slots();
15846 assert!(
15847 slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS),
15848 "declared_supervisor_slots must push \
15849 SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS when `:max-restarts` \
15850 is Some({max_restarts}) — the accessor and the \
15851 enumerator gate must route through the same \
15852 substrate-primitive typed dispatch on the outer \
15853 :max-restarts presence bit (got slots={slots:?})",
15854 );
15855 }
15856 let c = caixa_with_max_restarts(None);
15857 let slots = c.declared_supervisor_slots();
15858 assert!(
15859 !slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS),
15860 "declared_supervisor_slots must NOT push \
15861 SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS when `:max-restarts` is \
15862 None — the author-omitted arm must route through the \
15863 accessor's None-return unchanged (got slots={slots:?})",
15864 );
15865 }
15866
15867 #[test]
15868 fn supervisor_view_max_restarts_arm_routes_through_accessor() {
15869 // Composition pin: [`Caixa::supervisor_view`]'s per-`:max-restarts`
15870 // [`SupervisorSpec`] construction arm must key off
15871 // [`Caixa::max_restarts`]'s `unwrap_or(5)` fold, not the raw
15872 // `self.max_restarts.unwrap_or(5)` field-fold. Structurally: for
15873 // every `:kind Supervisor` `Caixa` carrying an author-declared
15874 // `Some(n)`, the composed [`SupervisorSpec`]'s `.max_restarts()`
15875 // must byte-equal `n`; and for a `:kind Supervisor` `Caixa`
15876 // carrying `None`, the composed [`SupervisorSpec`]'s
15877 // `.max_restarts()` must byte-equal the OTP-canonical `5`. Peer
15878 // of the sibling
15879 // `supervisor_view_estrategia_arm_routes_through_accessor`
15880 // (ed04d3c) composition pin.
15881 for max_restarts in [1u32, 5, 1000] {
15882 let c = caixa_supervisor_with_max_restarts_and_window(Some(max_restarts), None);
15883 let view = c.supervisor_view().expect(
15884 "supervisor_view must materialize a SupervisorSpec for a \
15885 :kind Supervisor Caixa carrying a Some(:max-restarts)",
15886 );
15887 assert_eq!(
15888 view.max_restarts(),
15889 max_restarts,
15890 "supervisor_view must carry the outer \
15891 Caixa::max_restarts() Some arm onto the composed \
15892 SupervisorSpec.max_restarts field verbatim (got {}, \
15893 expected {max_restarts})",
15894 view.max_restarts(),
15895 );
15896 }
15897 let c = caixa_supervisor_with_max_restarts_and_window(None, None);
15898 let view = c.supervisor_view().expect(
15899 "supervisor_view must materialize a SupervisorSpec for a \
15900 :kind Supervisor Caixa carrying a None :max-restarts",
15901 );
15902 assert_eq!(
15903 view.max_restarts(),
15904 5,
15905 "supervisor_view must project the outer \
15906 Caixa::max_restarts() None arm onto the OTP-canonical \
15907 {{intensity, 5, 60}} default (5) through the flat-spread \
15908 unwrap_or(5) fold (got {})",
15909 view.max_restarts(),
15910 );
15911 assert!(
15912 c.max_restarts().is_none(),
15913 "Caixa::max_restarts() must remain None on the author-\
15914 omitted arm — the supervisor_view fold must not mutate \
15915 the outer flat-spread presence bit",
15916 );
15917 }
15918
15919 #[test]
15920 fn supervisor_view_max_restarts_fallback_routes_through_lifted_default() {
15921 // Composition pin: [`Caixa::supervisor_view`]'s author-omitted
15922 // `:max-restarts` arm must degrade onto the substrate-canonical
15923 // [`crate::supervisor::SUPERVISOR_MAX_RESTARTS_DEFAULT`] typed
15924 // `pub const` — the Erlang/OTP-canonical `{intensity, 5, 60}`
15925 // `MaxIntensity` default — rather than a raw `5` literal. Prior
15926 // to the lift the composition site carried an inline
15927 // `.unwrap_or(5)` with no compile-time link back to the shared
15928 // OTP-canonical default that the serde-side
15929 // `#[serde(default = "default_max_restarts")]` wire-format arm
15930 // and the [`Default for crate::supervisor::SupervisorSpec`]
15931 // struct-literal default arm both key off — so a future rebrand
15932 // of the OTP-canonical default (Elixir's `Supervisor` `3`
15933 // default, a per-cluster overlay the operator pins through the
15934 // MESH-COMPOSITION §III.2 supervision-canary
15935 // `:supervisor :max-restarts-overrides` roadmap slot) would
15936 // have had to be threaded through both the serde-side helper
15937 // and this view-construction arm in lockstep or a `:kind
15938 // Supervisor` caixa carrying `:max-restarts ()` would silently
15939 // resolve to a `SupervisorSpec` whose `max_restarts` disagreed
15940 // with the same fixture's serde-side `SupervisorSpec` view (an
15941 // author-omitted slot round-tripping through
15942 // `SupervisorSpec::default()` to the lifted constant, then
15943 // splitting to a stale literal past `supervisor_view`).
15944 // Byte-parity against the lifted constant closes the split.
15945 // Peer of the sibling
15946 // [`crate::supervisor::default_max_restarts_helper_routes_through_lifted_default`]
15947 // + [`crate::supervisor::supervisor_spec_default_max_restarts_routes_through_lifted_default`]
15948 // composition pins that close the same routing on the two
15949 // sibling entry points onto the shared substrate constant.
15950 let c = caixa_supervisor_with_max_restarts_and_window(None, None);
15951 let view = c.supervisor_view().expect(
15952 "supervisor_view must materialize a SupervisorSpec for a \
15953 :kind Supervisor Caixa carrying a None :max-restarts",
15954 );
15955 assert_eq!(
15956 view.max_restarts(),
15957 crate::supervisor::SUPERVISOR_MAX_RESTARTS_DEFAULT,
15958 "supervisor_view must degrade the outer \
15959 Caixa::max_restarts() None arm onto the lifted \
15960 SUPERVISOR_MAX_RESTARTS_DEFAULT typed pub const (got {}, \
15961 expected {})",
15962 view.max_restarts(),
15963 crate::supervisor::SUPERVISOR_MAX_RESTARTS_DEFAULT,
15964 );
15965 }
15966
15967 #[test]
15968 fn restart_window_returns_restart_window_option_verbatim_across_permutations() {
15969 // Value-shape pin: [`Caixa::restart_window`] returns the
15970 // `:restart-window` typed `Option<String>` verbatim as an
15971 // `Option<&str>`, borrowed from the typed slot's own storage,
15972 // byte-equal across the author-omitted `None` arm and each of
15973 // the representative fixtures in the accept-set — the canonical
15974 // `"60s"` from `{intensity, 5, 60}`, the sibling
15975 // canonical-magnitude forms (`"5m"` / `"1h"` / `"500ms"` / `"30"`
15976 // / `"0s"`) the shared codec's positive-set sweep pin covers,
15977 // plus a past-the-guard sentinel (`"1.5s"` — the fractional-
15978 // seconds drift the sibling [`Self::validate_restart_window`]
15979 // gate refuses; the accessor must ship the raw slot verbatim
15980 // so struct-literal fixtures continue to expose the drift at
15981 // the accessor boundary). Third outer top-level [`Caixa`]
15982 // supervisor-tree flat-spread pin — extends the sub-family onto
15983 // the sibling `Option<&str>` raw-duration-string arm.
15984 for window in [
15985 None,
15986 Some("60s"),
15987 Some("5m"),
15988 Some("1h"),
15989 Some("500ms"),
15990 Some("1.5s"),
15991 Some(""),
15992 ] {
15993 let c = caixa_with_restart_window(window);
15994 assert_eq!(
15995 c.restart_window(),
15996 window,
15997 "Caixa::restart_window must return :restart-window \
15998 verbatim as Option<&str> (got {:?}, expected {window:?})",
15999 c.restart_window(),
16000 );
16001 assert_eq!(
16002 c.restart_window(),
16003 c.restart_window.as_deref(),
16004 "Caixa::restart_window accessor and \
16005 self.restart_window.as_deref() field access must \
16006 byte-equal — a byte-level drift would silently split \
16007 the paired Caixa::declared_supervisor_slots \
16008 presence-probe arm from the \
16009 Caixa::validate_restart_window shared-codec gate and \
16010 the Caixa::supervisor_view soft-swallowing fold",
16011 );
16012 }
16013 }
16014
16015 #[test]
16016 fn restart_window_projects_slice_by_borrow() {
16017 // The by-borrow pin: [`Caixa::restart_window`] returns
16018 // `Option<&str>` by borrow — the returned string slice borrows
16019 // the underlying `Option<String>` storage of the `:restart-window`
16020 // slot and the accessor must not clone on every call. Peer of
16021 // the sibling outer top-level [`Caixa`] `Option<&str>`-return
16022 // by-borrow pins on the universal-axis scalar family
16023 // (`licenca_projects_option_ref_by_borrow` /
16024 // `descricao_projects_option_ref_by_borrow` and siblings) —
16025 // extended onto the M2 supervisor-tree flat-spread
16026 // `Option<&str>` raw-duration-string axis.
16027 for window in [None, Some("60s"), Some("5m"), Some("")] {
16028 let c = caixa_with_restart_window(window);
16029 let first = c.restart_window();
16030 let second = c.restart_window();
16031 assert_eq!(
16032 first, second,
16033 "Caixa::restart_window must be idempotent — two \
16034 successive calls on the same &self must return the \
16035 same Option<&str>",
16036 );
16037 if let (Some(a), Some(b)) = (first, second) {
16038 assert_eq!(
16039 a.as_ptr(),
16040 b.as_ptr(),
16041 "Caixa::restart_window must borrow the underlying \
16042 String storage — two successive Some-arm calls must \
16043 return slices with the same backing pointer (a fresh \
16044 String clone would change the pointer on every call)",
16045 );
16046 }
16047 assert_eq!(
16048 first, window,
16049 "Caixa::restart_window must return :restart-window \
16050 verbatim by borrow — got {first:?}, expected {window:?}",
16051 );
16052 }
16053 }
16054
16055 #[test]
16056 fn declared_supervisor_slots_restart_window_arm_routes_through_accessor() {
16057 // Composition pin: [`Caixa::declared_supervisor_slots`]'s
16058 // `:restart-window` presence-probe arm must key off
16059 // [`Caixa::restart_window`], not the raw
16060 // `self.restart_window.is_some()` field-probe. Structurally:
16061 // every `Caixa { restart_window: Some(_), .. }` must push
16062 // `SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW` onto the declared-slot
16063 // list, and a `Caixa { restart_window: None, .. }` must NOT
16064 // push the label. Peer of the sibling
16065 // `declared_supervisor_slots_max_restarts_arm_routes_through_accessor`
16066 // routing pin.
16067 for window in ["60s", "5m", "1h", "500ms", "1.5s", ""] {
16068 let c = caixa_with_restart_window(Some(window));
16069 let slots = c.declared_supervisor_slots();
16070 assert!(
16071 slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW),
16072 "declared_supervisor_slots must push \
16073 SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW when \
16074 `:restart-window` is Some({window:?}) — the accessor \
16075 and the enumerator gate must route through the same \
16076 substrate-primitive typed dispatch on the outer \
16077 :restart-window presence bit (got slots={slots:?})",
16078 );
16079 }
16080 let c = caixa_with_restart_window(None);
16081 let slots = c.declared_supervisor_slots();
16082 assert!(
16083 !slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW),
16084 "declared_supervisor_slots must NOT push \
16085 SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW when `:restart-window` \
16086 is None — the author-omitted arm must route through the \
16087 accessor's None-return unchanged (got slots={slots:?})",
16088 );
16089 }
16090
16091 #[test]
16092 fn validate_restart_window_arm_routes_through_accessor() {
16093 // Composition pin: [`Caixa::validate_restart_window`]'s
16094 // shared-codec fold arm must key off [`Caixa::restart_window`],
16095 // not the raw `self.restart_window.as_deref()` field-projection.
16096 // Structurally: (1) `None` → `Ok(())` (the "omit the slot to
16097 // express no reset" canonical shape); (2) a canonical `Some`
16098 // arm (`"60s"`) → `Ok(())`; (3) a codec-rejected `Some` arm
16099 // (`"1.5s"`) → `Err(RestartWindowMalformed { restart_window,
16100 // .. })` carrying the offending raw string verbatim. The three
16101 // arms jointly pin that the validator's raw-string binding is
16102 // the accessor's return, not a peer projection — any future
16103 // silent detour that had the accessor collapse `Some("")` to
16104 // `None` would silently absorb the empty-after-trim refusal
16105 // case at the accessor boundary.
16106 caixa_with_restart_window(None)
16107 .validate_restart_window()
16108 .expect("None :restart-window must validate through the accessor");
16109 caixa_with_restart_window(Some("60s"))
16110 .validate_restart_window()
16111 .expect("canonical :restart-window \"60s\" must validate through the accessor");
16112 let err = caixa_with_restart_window(Some("1.5s"))
16113 .validate_restart_window()
16114 .expect_err("fractional-seconds :restart-window must fail through the accessor");
16115 assert!(
16116 matches!(
16117 err,
16118 ManifestError::RestartWindowMalformed { ref restart_window, .. }
16119 if restart_window == "1.5s"
16120 ),
16121 "validator must carry the offending raw string verbatim \
16122 from the accessor's borrowed &str (got {err:?})",
16123 );
16124 }
16125
16126 #[test]
16127 fn supervisor_view_restart_window_arm_routes_through_accessor() {
16128 // Composition pin: [`Caixa::supervisor_view`]'s
16129 // per-`:restart-window` [`SupervisorSpec`] construction arm
16130 // must key off [`Caixa::restart_window`]'s soft-swallowing
16131 // `.and_then(|s| duration_codec::parse(s).ok())` fold, not the
16132 // raw `self.restart_window.as_deref().and_then(…)` field-fold.
16133 // Structurally: (1) `None` → `SupervisorSpec.restart_window ==
16134 // None` (the "never reset" sentinel); (2) canonical `Some("60s")`
16135 // → `SupervisorSpec.restart_window == Some(Duration::from_secs(60))`
16136 // (the shared codec's canonical parse); (3) codec-rejected
16137 // `Some("1.5s")` → `SupervisorSpec.restart_window == None`
16138 // (the soft-swallow preserving the view's best-effort shape).
16139 let c = caixa_supervisor_with_max_restarts_and_window(None, None);
16140 let view = c.supervisor_view().expect("Supervisor kind has a view");
16141 assert_eq!(
16142 view.restart_window(),
16143 None,
16144 "supervisor_view must project outer None :restart-window \
16145 onto None on the composed SupervisorSpec (never-reset \
16146 sentinel) through the accessor's None-return unchanged",
16147 );
16148
16149 let c = caixa_supervisor_with_max_restarts_and_window(None, Some("60s"));
16150 let view = c.supervisor_view().expect("Supervisor kind has a view");
16151 assert_eq!(
16152 view.restart_window(),
16153 Some(std::time::Duration::from_secs(60)),
16154 "supervisor_view must fold outer Some(\"60s\") through the \
16155 shared duration_codec into Duration::from_secs(60) on the \
16156 composed SupervisorSpec (accessor's Some(&str) → codec \
16157 parse → Some(Duration))",
16158 );
16159
16160 let c = caixa_supervisor_with_max_restarts_and_window(None, Some("1.5s"));
16161 let view = c.supervisor_view().expect("Supervisor kind has a view");
16162 assert_eq!(
16163 view.restart_window(),
16164 None,
16165 "supervisor_view must soft-swallow the shared-codec parse \
16166 failure to None (the view's best-effort shape the sibling \
16167 manifest-level validate_restart_window surfaces as \
16168 RestartWindowMalformed); the accessor's raw-string return \
16169 is the single input every downstream consumer keys off",
16170 );
16171 }
16172
16173 // ── Caixa::upgrade_from — outer top-level &[UpgradeFromEntry] composite-slice accessor ──
16174
16175 fn caixa_with_upgrade_from(upgrade_from: Vec<crate::upgrade::UpgradeFromEntry>) -> Caixa {
16176 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
16177 c.upgrade_from = upgrade_from;
16178 c
16179 }
16180
16181 #[test]
16182 fn upgrade_from_returns_upgrade_from_slice_verbatim_across_permutations() {
16183 // The canonical per-`Caixa` `:upgrade-from` M2 typed-slot
16184 // outer-composite `&[UpgradeFromEntry]`-return slice-shape
16185 // pin: [`Caixa::upgrade_from`] must return the `:upgrade-from`
16186 // typed `Vec<UpgradeFromEntry>` verbatim as a
16187 // `&[UpgradeFromEntry]` slice-view over the same backing
16188 // buffer the raw `self.upgrade_from.as_slice()` field access
16189 // borrows from, element-equal across every representative
16190 // fixture in the accept-set — `[]` (the "no hot-upgrade path
16191 // declared" arm every `defcaixa` without an `:upgrade-from`
16192 // block carries; `#[serde(default)]` folds an omitted slot
16193 // onto `Vec::new()`), a canonical single-entry `Restart`
16194 // fixture (the shape most Servicos carry — a single prior
16195 // version with the fallback strategy), a canonical multi-
16196 // entry list carrying every typed instruction variant
16197 // (`LoadModule` / `StateChange` / `SoftPurge` / `Purge` /
16198 // `Restart`), and a past-the-guard sentinel — a duplicate-
16199 // `:from` `[(0.1.0, Restart), (0.1.0, Restart)]` entry pair
16200 // ([`crate::upgrade::validate_upgrade_from`] rejects through
16201 // `DuplicateFrom { from: "0.1.0" }` but the accessor must
16202 // ship the raw slot verbatim so struct-literal fixtures
16203 // continue to expose the duplicate at the accessor boundary).
16204 //
16205 // Pins against a future silent detour that returned an owned
16206 // `Vec<UpgradeFromEntry>` (which would type-check but silently
16207 // clone on every accessor call, breaking the zero-cost
16208 // projection every peer sibling slice accessor carries), a
16209 // `[dup, dup] → [dup]` dedup collapse (which would silently
16210 // absorb the `DuplicateFrom` refusal case at the accessor
16211 // boundary and the [`crate::StandardLayout::verify`] cross-
16212 // entry gate would silently accept a struct-literal `Caixa`
16213 // carrying the drift), a reference to an operator-resolved
16214 // overlay (the future per-cluster `:upgrade-overrides` slot
16215 // — its resolution must land at exactly this accessor body,
16216 // not silently divert the raw slot away from a second
16217 // consumer), or an axis-shuffled projection (a future detour
16218 // that reordered entries through the accessor would silently
16219 // split the paired [`crate::StandardLayout::verify`] per-
16220 // `:upgrade-from` shape gate's traversal input from the peer
16221 // [`crate::render::servico_m2_overlay`] emitter's projection
16222 // input, since the operator's hot-upgrade dispatch matches
16223 // per-`:from` and axis reordering would silently split the
16224 // per-entry script-path existence probe's iteration order
16225 // from the M2 overlay emitter's serialized-entry order).
16226 //
16227 // First outer top-level [`Caixa`] `&[Composite]`-return
16228 // slice accessor pin on the substrate primitive for M2 / M3
16229 // typed-slot vec-carry axes — opens the outer-`Caixa`
16230 // `&[Composite]` composite-slice projection pattern the
16231 // sibling `:children` [`crate::supervisor::ChildSpec`] /
16232 // `:membros` [`crate::aplicacao::Membro`] / `:contratos`
16233 // [`crate::aplicacao::WitContract`] future outer-composite-
16234 // slice pins fold on. Peer of the closed outer-`Caixa`
16235 // scalar `Option<&Composite>` composite-reference family the
16236 // sibling `limits` / `behavior` / `politicas` / `placement`
16237 // / `entrada` `..._returns_..._option_ref_verbatim_across_
16238 // permutations` pins closed (b2bd9d7 → e4128e4) — extends
16239 // the "byte-equal, borrow-shared" outer-accessor discipline
16240 // onto the outer-`Caixa` `&[Composite]` vec-carry altitude.
16241 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
16242 let fixtures: Vec<Vec<UpgradeFromEntry>> = vec![
16243 vec![],
16244 vec![UpgradeFromEntry {
16245 from: "0.0.1".into(),
16246 instructions: vec![UpgradeInstruction::Restart],
16247 }],
16248 vec![
16249 UpgradeFromEntry {
16250 from: "0.0.1".into(),
16251 instructions: vec![
16252 UpgradeInstruction::LoadModule {
16253 module: "demo".into(),
16254 },
16255 UpgradeInstruction::SoftPurge {
16256 module: "demo".into(),
16257 },
16258 ],
16259 },
16260 UpgradeFromEntry {
16261 from: "0.0.2".into(),
16262 instructions: vec![
16263 UpgradeInstruction::StateChange {
16264 script: "servicos/upgrade.lisp".into(),
16265 },
16266 UpgradeInstruction::Purge {
16267 module: "demo".into(),
16268 },
16269 UpgradeInstruction::Restart,
16270 ],
16271 },
16272 ],
16273 vec![
16274 UpgradeFromEntry {
16275 from: "0.1.0".into(),
16276 instructions: vec![UpgradeInstruction::Restart],
16277 },
16278 UpgradeFromEntry {
16279 from: "0.1.0".into(),
16280 instructions: vec![UpgradeInstruction::Restart],
16281 },
16282 ],
16283 ];
16284 for upgrade_from in fixtures {
16285 let c = caixa_with_upgrade_from(upgrade_from.clone());
16286 assert_eq!(
16287 c.upgrade_from(),
16288 upgrade_from.as_slice(),
16289 "Caixa::upgrade_from must return :upgrade-from \
16290 verbatim (got {:?}, expected {upgrade_from:?})",
16291 c.upgrade_from(),
16292 );
16293 assert_eq!(
16294 c.upgrade_from(),
16295 c.upgrade_from.as_slice(),
16296 "Caixa::upgrade_from must element-equal the raw \
16297 `self.upgrade_from.as_slice()` field access across \
16298 every value in the Vec<UpgradeFromEntry> accept-set",
16299 );
16300 assert_eq!(
16301 c.upgrade_from().is_empty(),
16302 c.upgrade_from.is_empty(),
16303 "Caixa::upgrade_from().is_empty() must byte-equal \
16304 self.upgrade_from.is_empty() — a presence-bit drift \
16305 would silently split the paired \
16306 Caixa::declared_servico_slots M2 declared-slot \
16307 enumerator's presence probe from the peer \
16308 crate::render::servico_m2_overlay M2 overlay \
16309 emitter's presence gate",
16310 );
16311 }
16312 }
16313
16314 #[test]
16315 fn declared_servico_slots_upgrade_from_arm_routes_through_accessor() {
16316 // Composition pin: [`Caixa::declared_servico_slots`]'s
16317 // `:upgrade-from` presence-probe arm must key off
16318 // [`Caixa::upgrade_from`], not the raw
16319 // `self.upgrade_from.is_empty()` field-probe. Structurally: a
16320 // `Caixa { upgrade_from: vec![UpgradeFromEntry { from: "0.0.1",
16321 // instructions: vec![Restart] }], .. }` must push
16322 // `M2_AUTHOR_KEY_UPGRADE_FROM` onto the declared-slot list
16323 // (the presence bit is non-empty, so the M2 kind-coherence
16324 // gate must surface the slot as "declared"), and a `Caixa {
16325 // upgrade_from: vec![], .. }` must NOT push the label (the
16326 // "author omitted the slot entirely" arm — the empty-slice
16327 // partition the serde-default folds onto). The pair jointly
16328 // pins the accessor + declared-slot enumerator composition:
16329 // any future silent detour that had the accessor collapse
16330 // `[Restart]` to `[]` (a `.filter(|e| !e.instructions.
16331 // is_empty())` projection) would silently absorb the
16332 // "declared but degenerate" arm at the accessor boundary and
16333 // the [`crate::LayoutError::ServicoSlotsOnNonServico`] kind-
16334 // coherence gate would silently accept a struct-literal
16335 // `Caixa` carrying the drift.
16336 //
16337 // Peer of the sibling
16338 // `declared_servico_slots_limits_arm_routes_through_accessor`
16339 // (b2bd9d7) and
16340 // `declared_servico_slots_behavior_arm_routes_through_accessor`
16341 // (35d8b52) composition pins on the sibling `:limits` /
16342 // `:behavior` outer-`Option<&Composite>` arms — same "the
16343 // enumerator gate must route through the substrate-primitive
16344 // typed dispatch" discipline extended onto the third M2
16345 // Servico-runtime slot axis, closing the enumerator's routing
16346 // invariant on every M2 arm.
16347 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
16348 let c = caixa_with_upgrade_from(vec![UpgradeFromEntry {
16349 from: "0.0.1".into(),
16350 instructions: vec![UpgradeInstruction::Restart],
16351 }]);
16352 let slots = c.declared_servico_slots();
16353 assert!(
16354 slots.contains(&crate::render::M2_AUTHOR_KEY_UPGRADE_FROM),
16355 "declared_servico_slots must push \
16356 M2_AUTHOR_KEY_UPGRADE_FROM when `:upgrade-from` is \
16357 non-empty — the accessor and the enumerator gate must \
16358 route through the same substrate-primitive typed \
16359 dispatch on the outer :upgrade-from presence bit (got \
16360 slots={slots:?})",
16361 );
16362 let c = caixa_with_upgrade_from(vec![]);
16363 let slots = c.declared_servico_slots();
16364 assert!(
16365 !slots.contains(&crate::render::M2_AUTHOR_KEY_UPGRADE_FROM),
16366 "declared_servico_slots must NOT push \
16367 M2_AUTHOR_KEY_UPGRADE_FROM when `:upgrade-from` is \
16368 empty — the author-omitted arm must route through the \
16369 accessor's empty-slice return unchanged (got \
16370 slots={slots:?})",
16371 );
16372 }
16373
16374 #[test]
16375 fn servico_m2_overlay_upgrade_from_arm_routes_through_accessor() {
16376 // Composition pin: [`crate::render::servico_m2_overlay`]'s
16377 // per-`:upgrade-from` M2 overlay emit arm must key off
16378 // [`Caixa::upgrade_from`], not the raw
16379 // `!caixa.upgrade_from.is_empty()` presence gate + the
16380 // `serde_yaml::to_value(&caixa.upgrade_from)` projection.
16381 // Structurally: a `Caixa { upgrade_from: vec![UpgradeFromEntry
16382 // { from: "0.0.1", instructions: vec![Restart] }], .. }` must
16383 // surface the `M2_KEY_UPGRADE_FROM` key with a per-entry
16384 // sequence in the overlay (the emitter fans onto the serde
16385 // slice-serialization), and a `Caixa { upgrade_from: vec![],
16386 // .. }` must omit the key entirely (the empty-slice
16387 // partition — the `!.is_empty()` outer gate elides the key
16388 // when the author omitted the slot). The pair jointly pins
16389 // the accessor + M2 overlay emitter composition: any future
16390 // silent detour that had the accessor return a fresh-cloned
16391 // `Vec<UpgradeFromEntry>` copy would silently break the
16392 // reference-identity pin the peer per-entry
16393 // `serde_yaml::to_value(caixa.upgrade_from())` projection
16394 // reads from — the projection would clone once per accessor
16395 // call instead of borrowing the storage buffer verbatim.
16396 //
16397 // Peer of the sibling
16398 // `servico_m2_overlay_limits_arm_routes_through_accessor`
16399 // (b2bd9d7) and
16400 // `servico_m2_overlay_behavior_arm_routes_through_accessor`
16401 // (35d8b52) composition pins on the sibling `:limits` /
16402 // `:behavior` outer-`Option<&Composite>` arms — same "the
16403 // M2 overlay emitter must route through the substrate-
16404 // primitive typed dispatch" discipline extended onto the
16405 // third M2 Servico-runtime slot axis, closing the overlay
16406 // emitter's routing invariant on every M2 arm.
16407 use crate::render::{M2_KEY_UPGRADE_FROM, servico_m2_overlay};
16408 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
16409 let c = caixa_with_upgrade_from(vec![UpgradeFromEntry {
16410 from: "0.0.1".into(),
16411 instructions: vec![UpgradeInstruction::Restart],
16412 }]);
16413 let overlay = servico_m2_overlay(&c).unwrap();
16414 assert!(
16415 overlay.contains_key(M2_KEY_UPGRADE_FROM),
16416 "servico_m2_overlay must surface M2_KEY_UPGRADE_FROM when \
16417 `:upgrade-from` is non-empty — the accessor and the M2 \
16418 overlay emitter must route through the same substrate- \
16419 primitive typed dispatch on the outer :upgrade-from \
16420 slice (got overlay={overlay:?})",
16421 );
16422 let c = caixa_with_upgrade_from(vec![]);
16423 let overlay = servico_m2_overlay(&c).unwrap();
16424 assert!(
16425 !overlay.contains_key(M2_KEY_UPGRADE_FROM),
16426 "servico_m2_overlay must omit M2_KEY_UPGRADE_FROM when \
16427 `:upgrade-from` is empty — the empty-slice partition \
16428 must route through the accessor's empty-slice return \
16429 unchanged (got overlay={overlay:?})",
16430 );
16431 }
16432
16433 #[test]
16434 fn upgrade_from_projects_slice_by_borrow() {
16435 // The by-borrow pin: [`Caixa::upgrade_from`] returns
16436 // `&[UpgradeFromEntry]` by borrow — the returned slice
16437 // borrows the underlying `Vec<UpgradeFromEntry>` storage of
16438 // the `:upgrade-from` slot and the accessor must not clone
16439 // the backing `Vec` on every call. Peer of the sibling
16440 // outer top-level [`Caixa`] `&[T]`-return by-borrow pins
16441 // (`autores_projects_slice_by_borrow` b5d813f,
16442 // `etiquetas_projects_slice_by_borrow` 78c7d3c,
16443 // `bibliotecas_projects_slice_by_borrow` 8a36c23,
16444 // `exe_projects_slice_by_borrow` 65d9527,
16445 // `servicos_projects_slice_by_borrow` 611f78b,
16446 // `deps_projects_slice_by_borrow` ad34b4e,
16447 // `deps_dev_projects_slice_by_borrow` f7fd81e) on the
16448 // sibling outer top-level [`Caixa`] scalar-element `&[T]`
16449 // axes — extended here to the first outer-`Caixa`
16450 // composite-element `&[Composite]` axis: the accessor's
16451 // returned slice must borrow from `&self` (the returned
16452 // reference's lifetime is tied to `&self`), and calling the
16453 // accessor twice on the same [`Caixa`] must yield slices
16454 // that are pointer-equal (the underlying byte-buffer is the
16455 // storage `Vec`'s allocation, not a fresh copy) as well as
16456 // value-equal (idempotent, no side effects on `&self`).
16457 //
16458 // Pins against a future silent detour that returned an owned
16459 // `Vec<UpgradeFromEntry>` (which would type-check but
16460 // silently clone on every call), a `&Vec<UpgradeFromEntry>`
16461 // return (which would leak the backing `Vec`'s
16462 // grow/push/reserve surface no downstream consumer reaches
16463 // for), or a one-arm-only accessor that returned a
16464 // saturating value on some sentinel input.
16465 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
16466 for upgrade_from in [
16467 vec![],
16468 vec![UpgradeFromEntry {
16469 from: "0.0.1".into(),
16470 instructions: vec![UpgradeInstruction::Restart],
16471 }],
16472 vec![
16473 UpgradeFromEntry {
16474 from: "0.0.1".into(),
16475 instructions: vec![UpgradeInstruction::Restart],
16476 },
16477 UpgradeFromEntry {
16478 from: "0.0.2".into(),
16479 instructions: vec![UpgradeInstruction::SoftPurge {
16480 module: "demo".into(),
16481 }],
16482 },
16483 ],
16484 ] {
16485 let c = caixa_with_upgrade_from(upgrade_from.clone());
16486 let first = c.upgrade_from();
16487 let second = c.upgrade_from();
16488 assert_eq!(
16489 first, second,
16490 "Caixa::upgrade_from must be idempotent — two \
16491 successive calls on the same &self must return the \
16492 same &[UpgradeFromEntry]",
16493 );
16494 assert_eq!(
16495 first.as_ptr(),
16496 second.as_ptr(),
16497 "Caixa::upgrade_from must borrow the underlying \
16498 Vec<UpgradeFromEntry> storage — two successive calls \
16499 must return slices with the same backing pointer (a \
16500 fresh Vec<UpgradeFromEntry> clone would change the \
16501 pointer on every call)",
16502 );
16503 assert_eq!(
16504 first,
16505 upgrade_from.as_slice(),
16506 "Caixa::upgrade_from must return :upgrade-from \
16507 verbatim by borrow — got {first:?}, expected \
16508 {upgrade_from:?}",
16509 );
16510 }
16511 }
16512
16513 // ── Caixa::children — outer top-level &[ChildSpec] composite-slice accessor ──
16514
16515 fn caixa_with_children(children: Vec<crate::supervisor::ChildSpec>) -> Caixa {
16516 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
16517 c.children = children;
16518 c
16519 }
16520
16521 #[test]
16522 fn children_returns_children_slice_verbatim_across_permutations() {
16523 // The canonical per-`Caixa` `:children` M2 supervisor-tree-slot
16524 // outer-composite `&[ChildSpec]`-return slice-shape pin:
16525 // [`Caixa::children`] must return the `:children` typed
16526 // `Vec<ChildSpec>` verbatim as a `&[ChildSpec]` slice-view over
16527 // the same backing buffer the raw `self.children.as_slice()`
16528 // field access borrows from, element-equal across every
16529 // representative fixture in the accept-set — `[]` (the "no
16530 // static children declared" arm every non-`Supervisor`-kind
16531 // `defcaixa` carries by `#[serde(default)]` and every
16532 // `SimpleOneForOne` supervisor carries by cross-slot refusal),
16533 // a canonical single-child `Permanent` fixture (the shape
16534 // most `OneForOne` supervisors carry — a single long-running
16535 // worker child), a canonical multi-child list carrying every
16536 // typed restart-policy variant (`Permanent` / `Transient` /
16537 // `Temporary`), and a past-the-guard sentinel — a duplicate
16538 // `:caixa` `[("w", ...), ("w", ...)]` entry pair
16539 // ([`crate::SupervisorSpec::validate`] rejects through
16540 // `DuplicateChildNome { nome: "w" }` but the accessor must
16541 // ship the raw slot verbatim so struct-literal fixtures
16542 // continue to expose the duplicate at the accessor boundary).
16543 //
16544 // Pins against a future silent detour that returned an owned
16545 // `Vec<ChildSpec>` (which would type-check but silently clone
16546 // on every accessor call, breaking the zero-cost projection
16547 // every peer sibling slice accessor carries), a `[dup, dup] →
16548 // [dup]` dedup collapse (which would silently absorb the
16549 // `DuplicateChildNome` refusal case at the accessor boundary
16550 // and the [`crate::StandardLayout::verify`] cross-child gate
16551 // would silently accept a struct-literal `Caixa` carrying the
16552 // drift), a reference to an operator-resolved overlay (the
16553 // future per-cluster `:children-overrides` slot — its
16554 // resolution must land at exactly this accessor body, not
16555 // silently divert the raw slot away from a second consumer),
16556 // or an axis-shuffled projection (a future detour that
16557 // reordered children through the accessor would silently
16558 // split the paired [`crate::StandardLayout::verify`] per-
16559 // supervisor gate's traversal input from the peer
16560 // [`Self::supervisor_view`] fold-in path's clone-order input,
16561 // since the OTP `RestForOne` restart strategy dispatches on
16562 // declared child order and axis reordering would silently
16563 // split the operator's per-cluster restart-fan-out order
16564 // from the caixa.lisp source-order).
16565 //
16566 // Second outer top-level [`Caixa`] `&[Composite]`-return slice
16567 // accessor pin on the substrate primitive for M2 / M3 typed-
16568 // slot vec-carry axes — folds on the outer-`Caixa`
16569 // `&[Composite]` composite-slice sub-family the sibling
16570 // `upgrade_from_returns_upgrade_from_slice_verbatim_across_permutations`
16571 // (2a1f907) pin opened, peer at the outer altitude of the
16572 // closed inner-`SupervisorSpec` `SupervisorSpec::children`
16573 // (bc92bce) accessor on the same OTP-supervisor static-child-
16574 // list axis.
16575 use crate::supervisor::{ChildSpec, RestartPolicy};
16576 let fixtures: Vec<Vec<ChildSpec>> = vec![
16577 vec![],
16578 vec![ChildSpec {
16579 caixa: "worker".into(),
16580 versao: "^0.1".into(),
16581 restart: RestartPolicy::Permanent,
16582 }],
16583 vec![
16584 ChildSpec {
16585 caixa: "worker-a".into(),
16586 versao: "^0.1".into(),
16587 restart: RestartPolicy::Permanent,
16588 },
16589 ChildSpec {
16590 caixa: "worker-b".into(),
16591 versao: "^0.1".into(),
16592 restart: RestartPolicy::Transient,
16593 },
16594 ChildSpec {
16595 caixa: "worker-c".into(),
16596 versao: "^0.1".into(),
16597 restart: RestartPolicy::Temporary,
16598 },
16599 ],
16600 vec![
16601 ChildSpec {
16602 caixa: "w".into(),
16603 versao: "^0.1".into(),
16604 restart: RestartPolicy::Permanent,
16605 },
16606 ChildSpec {
16607 caixa: "w".into(),
16608 versao: "^0.1".into(),
16609 restart: RestartPolicy::Permanent,
16610 },
16611 ],
16612 ];
16613 for children in fixtures {
16614 let c = caixa_with_children(children.clone());
16615 assert_eq!(
16616 c.children(),
16617 children.as_slice(),
16618 "Caixa::children must return :children verbatim \
16619 (got {:?}, expected {children:?})",
16620 c.children(),
16621 );
16622 assert_eq!(
16623 c.children(),
16624 c.children.as_slice(),
16625 "Caixa::children must element-equal the raw \
16626 `self.children.as_slice()` field access across \
16627 every value in the Vec<ChildSpec> accept-set",
16628 );
16629 assert_eq!(
16630 c.children().is_empty(),
16631 c.children.is_empty(),
16632 "Caixa::children().is_empty() must byte-equal \
16633 self.children.is_empty() — a presence-bit drift \
16634 would silently split the paired \
16635 Caixa::declared_supervisor_slots supervisor-tree \
16636 declared-slot enumerator's presence probe from the \
16637 peer Caixa::supervisor_view typed-view composer's \
16638 fold-in path",
16639 );
16640 }
16641 }
16642
16643 #[test]
16644 fn declared_supervisor_slots_children_arm_routes_through_accessor() {
16645 // Composition pin: [`Caixa::declared_supervisor_slots`]'s
16646 // `:children` presence-probe arm must key off
16647 // [`Caixa::children`], not the raw
16648 // `!self.children.is_empty()` field-probe. Structurally: a
16649 // `Caixa { children: vec![ChildSpec { caixa: "w", versao:
16650 // "^0.1", restart: Permanent }], .. }` must push
16651 // `SUPERVISOR_AUTHOR_KEY_CHILDREN` onto the declared-slot list
16652 // (the presence bit is non-empty, so the supervisor-tree
16653 // kind-coherence gate must surface the slot as "declared"),
16654 // and a `Caixa { children: vec![], .. }` must NOT push the
16655 // label (the "author omitted the slot entirely" arm — the
16656 // empty-slice partition the serde-default folds onto). The
16657 // pair jointly pins the accessor + declared-slot enumerator
16658 // composition: any future silent detour that had the accessor
16659 // collapse `[Permanent]` to `[]` (a `.filter(|c| c.nome() !=
16660 // "__reserved__")` projection) would silently absorb the
16661 // "declared but degenerate" arm at the accessor boundary and
16662 // the [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
16663 // kind-coherence gate would silently accept a struct-literal
16664 // `Caixa` carrying the drift.
16665 //
16666 // Peer of the sibling
16667 // `declared_servico_slots_upgrade_from_arm_routes_through_accessor`
16668 // (2a1f907) on the M2 `:upgrade-from` composite-slice arm —
16669 // same "the enumerator gate must route through the substrate-
16670 // primitive typed dispatch" discipline extended onto the
16671 // supervisor-tree `:children` composite-slice arm.
16672 use crate::supervisor::{ChildSpec, RestartPolicy};
16673 let c = caixa_with_children(vec![ChildSpec {
16674 caixa: "w".into(),
16675 versao: "^0.1".into(),
16676 restart: RestartPolicy::Permanent,
16677 }]);
16678 let slots = c.declared_supervisor_slots();
16679 assert!(
16680 slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN),
16681 "declared_supervisor_slots must push \
16682 SUPERVISOR_AUTHOR_KEY_CHILDREN when `:children` is \
16683 non-empty — the accessor and the enumerator gate must \
16684 route through the same substrate-primitive typed \
16685 dispatch on the outer :children presence bit (got \
16686 slots={slots:?})",
16687 );
16688 let c = caixa_with_children(vec![]);
16689 let slots = c.declared_supervisor_slots();
16690 assert!(
16691 !slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN),
16692 "declared_supervisor_slots must NOT push \
16693 SUPERVISOR_AUTHOR_KEY_CHILDREN when `:children` is \
16694 empty — the author-omitted arm must route through the \
16695 accessor's empty-slice return unchanged (got \
16696 slots={slots:?})",
16697 );
16698 }
16699
16700 #[test]
16701 fn supervisor_view_children_arm_routes_through_accessor() {
16702 // Composition pin: [`Caixa::supervisor_view`]'s per-`:children`
16703 // fold-in arm must key off [`Caixa::children`], not the raw
16704 // `self.children.clone()` field-clone. Structurally: a `Caixa {
16705 // kind: Supervisor, estrategia: Some(OneForOne), children:
16706 // vec![ChildSpec { caixa: "w", .. }], .. }` must fold the
16707 // per-child list through the accessor into the typed
16708 // [`SupervisorSpec`] view's `children` field verbatim — every
16709 // entry the accessor surfaces must land in the view's
16710 // `children` slot in the same order. The pair jointly pins the
16711 // accessor + view-composer composition: any future silent
16712 // detour that had the accessor return a fresh-cloned
16713 // `Vec<ChildSpec>` copy would silently break the reference-
16714 // identity pin the peer `supervisor_view` fold-in path reads
16715 // from — the fold would clone once more per accessor call
16716 // instead of borrowing the storage buffer verbatim once.
16717 //
16718 // Peer of the sibling
16719 // `supervisor_view_kind_gate_routes_through_accessor` (35d8b52-
16720 // family) composition pin on the peer kind-gate arm — same
16721 // "the view composer must route through the substrate-
16722 // primitive typed dispatch" discipline extended onto the
16723 // per-`:children` fold-in arm, closing the supervisor-view
16724 // composer's routing invariant on the composite-slice input.
16725 use crate::supervisor::{ChildSpec, RestartPolicy, RestartStrategy};
16726 let mut c = caixa_with_children(vec![
16727 ChildSpec {
16728 caixa: "worker-a".into(),
16729 versao: "^0.1".into(),
16730 restart: RestartPolicy::Permanent,
16731 },
16732 ChildSpec {
16733 caixa: "worker-b".into(),
16734 versao: "^0.1".into(),
16735 restart: RestartPolicy::Transient,
16736 },
16737 ]);
16738 c.kind = crate::CaixaKind::Supervisor;
16739 c.estrategia = Some(RestartStrategy::OneForOne);
16740 let view = c
16741 .supervisor_view()
16742 .expect("Supervisor kind must produce a supervisor_view");
16743 assert_eq!(
16744 view.children(),
16745 c.children(),
16746 "supervisor_view must fold Caixa::children verbatim into \
16747 SupervisorSpec::children — the accessor and the view \
16748 composer must route through the same substrate-primitive \
16749 typed dispatch on the outer :children slice (got view \
16750 children={:?}, expected {:?})",
16751 view.children(),
16752 c.children(),
16753 );
16754 }
16755
16756 #[test]
16757 fn children_projects_slice_by_borrow() {
16758 // The by-borrow pin: [`Caixa::children`] returns
16759 // `&[ChildSpec]` by borrow — the returned slice borrows the
16760 // underlying `Vec<ChildSpec>` storage of the `:children` slot
16761 // and the accessor must not clone the backing `Vec` on every
16762 // call. Peer of the sibling outer top-level [`Caixa`]
16763 // `&[T]`-return by-borrow pins (`autores_projects_slice_by_borrow`
16764 // b5d813f, `etiquetas_projects_slice_by_borrow` 78c7d3c,
16765 // `bibliotecas_projects_slice_by_borrow` 8a36c23,
16766 // `exe_projects_slice_by_borrow` 65d9527,
16767 // `servicos_projects_slice_by_borrow` 611f78b,
16768 // `deps_projects_slice_by_borrow` ad34b4e,
16769 // `deps_dev_projects_slice_by_borrow` f7fd81e,
16770 // `upgrade_from_projects_slice_by_borrow` 2a1f907) on the
16771 // sibling outer top-level [`Caixa`] scalar-element and
16772 // composite-element `&[T]` axes — folds on the outer-`Caixa`
16773 // composite-element `&[Composite]` axis: the accessor's
16774 // returned slice must borrow from `&self` (the returned
16775 // reference's lifetime is tied to `&self`), and calling the
16776 // accessor twice on the same [`Caixa`] must yield slices
16777 // that are pointer-equal (the underlying byte-buffer is the
16778 // storage `Vec`'s allocation, not a fresh copy) as well as
16779 // value-equal (idempotent, no side effects on `&self`).
16780 //
16781 // Pins against a future silent detour that returned an owned
16782 // `Vec<ChildSpec>` (which would type-check but silently clone
16783 // on every call), a `&Vec<ChildSpec>` return (which would leak
16784 // the backing `Vec`'s grow/push/reserve surface no downstream
16785 // consumer reaches for), or a one-arm-only accessor that
16786 // returned a saturating value on some sentinel input.
16787 use crate::supervisor::{ChildSpec, RestartPolicy};
16788 for children in [
16789 vec![],
16790 vec![ChildSpec {
16791 caixa: "w".into(),
16792 versao: "^0.1".into(),
16793 restart: RestartPolicy::Permanent,
16794 }],
16795 vec![
16796 ChildSpec {
16797 caixa: "worker-a".into(),
16798 versao: "^0.1".into(),
16799 restart: RestartPolicy::Permanent,
16800 },
16801 ChildSpec {
16802 caixa: "worker-b".into(),
16803 versao: "^0.1".into(),
16804 restart: RestartPolicy::Transient,
16805 },
16806 ],
16807 ] {
16808 let c = caixa_with_children(children.clone());
16809 let first = c.children();
16810 let second = c.children();
16811 assert_eq!(
16812 first, second,
16813 "Caixa::children must be idempotent — two successive \
16814 calls on the same &self must return the same \
16815 &[ChildSpec]",
16816 );
16817 assert_eq!(
16818 first.as_ptr(),
16819 second.as_ptr(),
16820 "Caixa::children must borrow the underlying \
16821 Vec<ChildSpec> storage — two successive calls must \
16822 return slices with the same backing pointer (a fresh \
16823 Vec<ChildSpec> clone would change the pointer on \
16824 every call)",
16825 );
16826 assert_eq!(
16827 first,
16828 children.as_slice(),
16829 "Caixa::children must return :children verbatim by \
16830 borrow — got {first:?}, expected {children:?}",
16831 );
16832 }
16833 }
16834
16835 // ── Caixa::membros — outer top-level &[Membro] composite-slice accessor ──
16836
16837 fn caixa_aplicacao_with_membros(membros: Vec<crate::aplicacao::Membro>) -> Caixa {
16838 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
16839 c.kind = CaixaKind::Aplicacao;
16840 c.membros = membros;
16841 c
16842 }
16843
16844 #[test]
16845 fn membros_returns_membros_slice_verbatim_across_permutations() {
16846 // The canonical per-`Caixa` `:membros` M3 mesh-slot outer-
16847 // composite `&[Membro]`-return slice-shape pin:
16848 // [`Caixa::membros`] must return the `:membros` typed
16849 // `Vec<Membro>` verbatim as a `&[Membro]` slice-view over the
16850 // same backing buffer the raw `self.membros.as_slice()` field
16851 // access borrows from, element-equal across every
16852 // representative fixture in the accept-set — `[]` (the "no
16853 // members declared" arm every non-`Aplicacao`-kind `defcaixa`
16854 // carries by `#[serde(default)]` and every partially-authored
16855 // Aplicacao carries before the
16856 // [`crate::AplicacaoError::MembrosEmpty`] gate fires), a
16857 // canonical single-member fixture (the shape a minimal
16858 // Aplicacao carries — one Servico wrapping one contained
16859 // computation), a canonical multi-member list carrying three
16860 // distinct entries (the canonical checkout-shape Aplicacao —
16861 // cart / pricing / auth — every canonical example carries), and
16862 // a past-the-guard sentinel — a duplicate `:caixa`
16863 // `[("cart", ...), ("cart", ...)]` entry pair
16864 // ([`crate::AplicacaoSpec::validate`] rejects through
16865 // `DuplicateMembro { nome: "cart" }` but the accessor must ship
16866 // the raw slot verbatim so struct-literal fixtures continue to
16867 // expose the duplicate at the accessor boundary).
16868 //
16869 // Pins against a future silent detour that returned an owned
16870 // `Vec<Membro>` (which would type-check but silently clone on
16871 // every accessor call, breaking the zero-cost projection every
16872 // peer sibling slice accessor carries), a `[dup, dup] → [dup]`
16873 // dedup collapse (which would silently absorb the
16874 // `DuplicateMembro` refusal case at the accessor boundary and
16875 // the [`crate::StandardLayout::verify`] cross-member gate would
16876 // silently accept a struct-literal `Caixa` carrying the drift),
16877 // a reference to an operator-resolved overlay (the future per-
16878 // cluster `:membros-overrides` slot — its resolution must land
16879 // at exactly this accessor body, not silently divert the raw
16880 // slot away from a second consumer), or an axis-shuffled
16881 // projection (a future detour that reordered members through
16882 // the accessor would silently split the paired
16883 // [`crate::StandardLayout::verify`] per-Aplicacao gate's
16884 // traversal input from the peer [`Self::aplicacao_view`] fold-
16885 // in path's clone-order input, since the canonical `:contratos`
16886 // `:de`/`:para` and `:entrada :para` cross-slot refusal probes
16887 // read the member set through the same slice).
16888 //
16889 // Third outer top-level [`Caixa`] `&[Composite]`-return slice
16890 // accessor pin on the substrate primitive for M2 / M3 typed-
16891 // slot vec-carry axes — opens the outer-`Caixa` M3 mesh-slot
16892 // arm of the `&[Composite]` composite-slice sub-family the
16893 // sibling M2 `upgrade_from_returns_upgrade_from_slice_verbatim_across_permutations`
16894 // (2a1f907) and
16895 // `children_returns_children_slice_verbatim_across_permutations`
16896 // (c17b51e) pins opened, peer at the outer altitude of the
16897 // closed inner-[`crate::AplicacaoSpec::membros`] (6c77e36)
16898 // accessor on the same MESH-COMPOSITION per-Aplicacao member-
16899 // list axis.
16900 use crate::aplicacao::Membro;
16901 let fixtures: Vec<Vec<Membro>> = vec![
16902 vec![],
16903 vec![Membro {
16904 caixa: "cart".into(),
16905 versao: "^0.1".into(),
16906 }],
16907 vec![
16908 Membro {
16909 caixa: "cart".into(),
16910 versao: "^0.1".into(),
16911 },
16912 Membro {
16913 caixa: "pricing".into(),
16914 versao: "^0.2".into(),
16915 },
16916 Membro {
16917 caixa: "auth".into(),
16918 versao: "^1.0".into(),
16919 },
16920 ],
16921 vec![
16922 Membro {
16923 caixa: "cart".into(),
16924 versao: "^0.1".into(),
16925 },
16926 Membro {
16927 caixa: "cart".into(),
16928 versao: "^0.1".into(),
16929 },
16930 ],
16931 ];
16932 for membros in fixtures {
16933 let c = caixa_aplicacao_with_membros(membros.clone());
16934 assert_eq!(
16935 c.membros(),
16936 membros.as_slice(),
16937 "Caixa::membros must return :membros verbatim \
16938 (got {:?}, expected {membros:?})",
16939 c.membros(),
16940 );
16941 assert_eq!(
16942 c.membros(),
16943 c.membros.as_slice(),
16944 "Caixa::membros must element-equal the raw \
16945 `self.membros.as_slice()` field access across every \
16946 value in the Vec<Membro> accept-set",
16947 );
16948 assert_eq!(
16949 c.membros().is_empty(),
16950 c.membros.is_empty(),
16951 "Caixa::membros().is_empty() must byte-equal \
16952 self.membros.is_empty() — a presence-bit drift would \
16953 silently split the paired Caixa::declared_mesh_slots \
16954 mesh declared-slot enumerator's presence probe from \
16955 the peer Caixa::aplicacao_view typed-view composer's \
16956 fold-in path",
16957 );
16958 }
16959 }
16960
16961 #[test]
16962 fn declared_mesh_slots_membros_arm_routes_through_accessor() {
16963 // Composition pin: [`Caixa::declared_mesh_slots`]'s `:membros`
16964 // presence-probe arm must key off [`Caixa::membros`], not the
16965 // raw `!self.membros.is_empty()` field-probe. Structurally: a
16966 // `Caixa { membros: vec![Membro { caixa: "cart", versao:
16967 // "^0.1" }], .. }` must push `M3_AUTHOR_KEY_MEMBROS` onto the
16968 // declared-slot list (the presence bit is non-empty, so the
16969 // mesh kind-coherence gate must surface the slot as
16970 // "declared"), and a `Caixa { membros: vec![], .. }` must NOT
16971 // push the label (the "author omitted the slot entirely" arm
16972 // — the empty-slice partition the serde-default folds onto).
16973 // The pair jointly pins the accessor + declared-slot
16974 // enumerator composition: any future silent detour that had
16975 // the accessor collapse `[Membro { .. }]` to `[]` (a
16976 // `.filter(|m| m.nome() != "__reserved__")` projection) would
16977 // silently absorb the "declared but degenerate" arm at the
16978 // accessor boundary and the
16979 // [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
16980 // coherence gate would silently accept a struct-literal
16981 // `Caixa` carrying the drift.
16982 //
16983 // Peer of the sibling
16984 // `declared_servico_slots_upgrade_from_arm_routes_through_accessor`
16985 // (2a1f907) and
16986 // `declared_supervisor_slots_children_arm_routes_through_accessor`
16987 // (c17b51e) composition pins on the M2 `:upgrade-from` /
16988 // `:children` composite-slice arms — same "the enumerator gate
16989 // must route through the substrate-primitive typed dispatch"
16990 // discipline extended onto the M3 `:membros` composite-slice
16991 // arm, opening the M3 arm of the declared-slot enumerator's
16992 // routing invariant.
16993 use crate::aplicacao::Membro;
16994 let c = caixa_aplicacao_with_membros(vec![Membro {
16995 caixa: "cart".into(),
16996 versao: "^0.1".into(),
16997 }]);
16998 let slots = c.declared_mesh_slots();
16999 assert!(
17000 slots.contains(&crate::render::M3_AUTHOR_KEY_MEMBROS),
17001 "declared_mesh_slots must push M3_AUTHOR_KEY_MEMBROS when \
17002 `:membros` is non-empty — the accessor and the enumerator \
17003 gate must route through the same substrate-primitive \
17004 typed dispatch on the outer :membros presence bit (got \
17005 slots={slots:?})",
17006 );
17007 let c = caixa_aplicacao_with_membros(vec![]);
17008 let slots = c.declared_mesh_slots();
17009 assert!(
17010 !slots.contains(&crate::render::M3_AUTHOR_KEY_MEMBROS),
17011 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_MEMBROS \
17012 when `:membros` is empty — the author-omitted arm must \
17013 route through the accessor's empty-slice return unchanged \
17014 (got slots={slots:?})",
17015 );
17016 }
17017
17018 #[test]
17019 fn aplicacao_view_membros_arm_routes_through_accessor() {
17020 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:membros`
17021 // fold-in arm must key off [`Caixa::membros`], not the raw
17022 // `self.membros.clone()` field-clone. Structurally: a `Caixa {
17023 // kind: Aplicacao, membros: vec![Membro { caixa: "cart", .. },
17024 // Membro { caixa: "pricing", .. }], .. }` must fold the per-
17025 // member list through the accessor into the typed
17026 // [`crate::AplicacaoSpec`] view's `membros` slot verbatim —
17027 // every entry the accessor surfaces must land in the view's
17028 // `membros` slot in the same order. The pair jointly pins the
17029 // accessor + view-composer composition: any future silent
17030 // detour that had the accessor return a fresh-cloned
17031 // `Vec<Membro>` copy would silently break the reference-
17032 // identity pin the peer `aplicacao_view` fold-in path reads
17033 // from — the fold would clone once more per accessor call
17034 // instead of borrowing the storage buffer verbatim once.
17035 //
17036 // Peer of the sibling
17037 // `aplicacao_view_politicas_arm_folds_through_accessor`
17038 // (5d23d29) /
17039 // `aplicacao_view_placement_arm_folds_through_accessor`
17040 // (4fb8074) /
17041 // `aplicacao_view_entrada_arm_folds_through_accessor` (e4128e4)
17042 // composition pins on the M3 `:politicas` / `:placement` /
17043 // `:entrada` outer-`Option<&Composite>` arms — extended here to
17044 // the M3 `:membros` outer-`&[Composite]` composite-slice arm,
17045 // closing the aplicacao-view composer's routing invariant on
17046 // the composite-slice input.
17047 use crate::aplicacao::Membro;
17048 let c = caixa_aplicacao_with_membros(vec![
17049 Membro {
17050 caixa: "cart".into(),
17051 versao: "^0.1".into(),
17052 },
17053 Membro {
17054 caixa: "pricing".into(),
17055 versao: "^0.2".into(),
17056 },
17057 ]);
17058 let view = c
17059 .aplicacao_view()
17060 .expect("Aplicacao kind must produce an aplicacao_view");
17061 assert_eq!(
17062 view.membros(),
17063 c.membros(),
17064 "aplicacao_view must fold Caixa::membros verbatim into \
17065 AplicacaoSpec::membros — the accessor and the view \
17066 composer must route through the same substrate-primitive \
17067 typed dispatch on the outer :membros slice (got view \
17068 membros={:?}, expected {:?})",
17069 view.membros(),
17070 c.membros(),
17071 );
17072 }
17073
17074 #[test]
17075 fn membros_projects_slice_by_borrow() {
17076 // The by-borrow pin: [`Caixa::membros`] returns `&[Membro]` by
17077 // borrow — the returned slice borrows the underlying
17078 // `Vec<Membro>` storage of the `:membros` slot and the
17079 // accessor must not clone the backing `Vec` on every call.
17080 // Peer of the sibling outer top-level [`Caixa`] `&[T]`-return
17081 // by-borrow pins (`autores_projects_slice_by_borrow` b5d813f,
17082 // `etiquetas_projects_slice_by_borrow` 78c7d3c,
17083 // `bibliotecas_projects_slice_by_borrow` 8a36c23,
17084 // `exe_projects_slice_by_borrow` 65d9527,
17085 // `servicos_projects_slice_by_borrow` 611f78b,
17086 // `deps_projects_slice_by_borrow` ad34b4e,
17087 // `deps_dev_projects_slice_by_borrow` f7fd81e,
17088 // `upgrade_from_projects_slice_by_borrow` 2a1f907,
17089 // `children_projects_slice_by_borrow` c17b51e) on the sibling
17090 // outer top-level [`Caixa`] scalar-element and composite-
17091 // element `&[T]` axes — folds on the outer-`Caixa` M3 mesh-
17092 // slot composite-element `&[Composite]` axis: the accessor's
17093 // returned slice must borrow from `&self` (the returned
17094 // reference's lifetime is tied to `&self`), and calling the
17095 // accessor twice on the same [`Caixa`] must yield slices that
17096 // are pointer-equal (the underlying byte-buffer is the storage
17097 // `Vec`'s allocation, not a fresh copy) as well as value-equal
17098 // (idempotent, no side effects on `&self`).
17099 //
17100 // Pins against a future silent detour that returned an owned
17101 // `Vec<Membro>` (which would type-check but silently clone on
17102 // every call), a `&Vec<Membro>` return (which would leak the
17103 // backing `Vec`'s grow/push/reserve surface no downstream
17104 // consumer reaches for), or a one-arm-only accessor that
17105 // returned a saturating value on some sentinel input.
17106 use crate::aplicacao::Membro;
17107 for membros in [
17108 vec![],
17109 vec![Membro {
17110 caixa: "cart".into(),
17111 versao: "^0.1".into(),
17112 }],
17113 vec![
17114 Membro {
17115 caixa: "cart".into(),
17116 versao: "^0.1".into(),
17117 },
17118 Membro {
17119 caixa: "pricing".into(),
17120 versao: "^0.2".into(),
17121 },
17122 ],
17123 ] {
17124 let c = caixa_aplicacao_with_membros(membros.clone());
17125 let first = c.membros();
17126 let second = c.membros();
17127 assert_eq!(
17128 first, second,
17129 "Caixa::membros must be idempotent — two successive \
17130 calls on the same &self must return the same &[Membro]",
17131 );
17132 assert_eq!(
17133 first.as_ptr(),
17134 second.as_ptr(),
17135 "Caixa::membros must borrow the underlying Vec<Membro> \
17136 storage — two successive calls must return slices with \
17137 the same backing pointer (a fresh Vec<Membro> clone \
17138 would change the pointer on every call)",
17139 );
17140 assert_eq!(
17141 first,
17142 membros.as_slice(),
17143 "Caixa::membros must return :membros verbatim by borrow \
17144 — got {first:?}, expected {membros:?}",
17145 );
17146 }
17147 }
17148
17149 // ── Caixa::contratos — outer top-level &[WitContract] composite-slice accessor ──
17150
17151 fn caixa_aplicacao_with_contratos(contratos: Vec<crate::aplicacao::WitContract>) -> Caixa {
17152 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
17153 c.kind = CaixaKind::Aplicacao;
17154 c.contratos = contratos;
17155 c
17156 }
17157
17158 fn contrato_http_for_test(
17159 de: &str,
17160 para: &str,
17161 endpoint: &str,
17162 ) -> crate::aplicacao::WitContract {
17163 crate::aplicacao::WitContract {
17164 de: de.into(),
17165 para: para.into(),
17166 wit: "wasi:http/proxy".into(),
17167 endpoint: Some(endpoint.into()),
17168 subject: None,
17169 slot: None,
17170 }
17171 }
17172
17173 #[test]
17174 fn contratos_returns_contratos_slice_verbatim_across_permutations() {
17175 // The canonical per-`Caixa` `:contratos` M3 mesh-slot outer-
17176 // composite `&[WitContract]`-return slice-shape pin:
17177 // [`Caixa::contratos`] must return the `:contratos` typed
17178 // `Vec<WitContract>` verbatim as a `&[WitContract]` slice-view
17179 // over the same backing buffer the raw
17180 // `self.contratos.as_slice()` field access borrows from,
17181 // element-equal across every representative fixture in the
17182 // accept-set — `[]` (the "no contracts declared" arm every
17183 // non-`Aplicacao`-kind `defcaixa` carries by
17184 // `#[serde(default)]` and every leaf-Aplicacao with a single
17185 // member carries), a canonical single-edge fixture (the
17186 // minimal directed-graph shape: one HTTP-shape `(cart → catalog)`
17187 // edge), and a canonical multi-edge fixture with three distinct
17188 // edges (the checkout-shape Aplicacao's HTTP-fan pattern:
17189 // `(cart → catalog)`, `(cart → pricing)`, `(cart → auth)`).
17190 //
17191 // Pins against a future silent detour that returned an owned
17192 // `Vec<WitContract>` (which would type-check but silently clone
17193 // on every accessor call, breaking the zero-cost projection
17194 // every peer sibling slice accessor carries), an axis-shuffled
17195 // projection (a future detour that reordered edges through the
17196 // accessor would silently split the paired
17197 // [`crate::StandardLayout::verify`] per-Aplicacao gate's
17198 // traversal input from the peer [`Self::aplicacao_view`] fold-
17199 // in path's clone-order input, since every canonical
17200 // `caixa-mesh` renderer's per-`(:de, :para)` adjacency-list
17201 // seed dispatch reads the edge set through the same slice),
17202 // or a reference to an operator-resolved overlay (the future
17203 // per-cluster `:contratos-overrides` slot — its resolution
17204 // must land at exactly this accessor body, not silently divert
17205 // the raw slot away from a second consumer).
17206 //
17207 // Fourth outer top-level [`Caixa`] `&[Composite]`-return slice
17208 // accessor pin on the substrate primitive for M2 / M3 typed-
17209 // slot vec-carry axes — closes the outer-`Caixa`
17210 // `&[Composite]` composite-slice sub-family the sibling M2
17211 // `upgrade_from_returns_upgrade_from_slice_verbatim_across_permutations`
17212 // (2a1f907) and
17213 // `children_returns_children_slice_verbatim_across_permutations`
17214 // (c17b51e) pins opened and the M3
17215 // `membros_returns_membros_slice_verbatim_across_permutations`
17216 // (0f26987) pin folded on, closing the outer-`Caixa` M3 mesh-
17217 // slot arm of the composite-slice sub-family. Peer at the outer
17218 // altitude of the closed inner-
17219 // [`crate::AplicacaoSpec::contratos`] (0dcc926) accessor on the
17220 // same MESH-COMPOSITION per-Aplicacao contract-list axis.
17221 let fixtures: Vec<Vec<crate::aplicacao::WitContract>> = vec![
17222 vec![],
17223 vec![contrato_http_for_test("cart", "catalog", "/items")],
17224 vec![
17225 contrato_http_for_test("cart", "catalog", "/items"),
17226 contrato_http_for_test("cart", "pricing", "/price"),
17227 contrato_http_for_test("cart", "auth", "/whoami"),
17228 ],
17229 ];
17230 for contratos in fixtures {
17231 let c = caixa_aplicacao_with_contratos(contratos.clone());
17232 assert_eq!(
17233 c.contratos(),
17234 contratos.as_slice(),
17235 "Caixa::contratos must return :contratos verbatim \
17236 (got {:?}, expected {contratos:?})",
17237 c.contratos(),
17238 );
17239 assert_eq!(
17240 c.contratos(),
17241 c.contratos.as_slice(),
17242 "Caixa::contratos must element-equal the raw \
17243 `self.contratos.as_slice()` field access across every \
17244 value in the Vec<WitContract> accept-set",
17245 );
17246 assert_eq!(
17247 c.contratos().is_empty(),
17248 c.contratos.is_empty(),
17249 "Caixa::contratos().is_empty() must byte-equal \
17250 self.contratos.is_empty() — a presence-bit drift would \
17251 silently split the paired Caixa::declared_mesh_slots \
17252 mesh declared-slot enumerator's presence probe from \
17253 the peer Caixa::aplicacao_view typed-view composer's \
17254 fold-in path",
17255 );
17256 }
17257 }
17258
17259 #[test]
17260 fn declared_mesh_slots_contratos_arm_routes_through_accessor() {
17261 // Composition pin: [`Caixa::declared_mesh_slots`]'s `:contratos`
17262 // presence-probe arm must key off [`Caixa::contratos`], not the
17263 // raw `!self.contratos.is_empty()` field-probe. Structurally: a
17264 // `Caixa { contratos: vec![WitContract { .. }], .. }` must push
17265 // `M3_AUTHOR_KEY_CONTRATOS` onto the declared-slot list (the
17266 // presence bit is non-empty, so the mesh kind-coherence gate
17267 // must surface the slot as "declared"), and a `Caixa {
17268 // contratos: vec![], .. }` must NOT push the label (the "author
17269 // omitted the slot entirely" arm — the empty-slice partition
17270 // the serde-default folds onto). The pair jointly pins the
17271 // accessor + declared-slot enumerator composition: any future
17272 // silent detour that had the accessor collapse
17273 // `[WitContract { .. }]` to `[]` (a `.filter(|c| c.de() !=
17274 // "__reserved__")` projection) would silently absorb the
17275 // "declared but degenerate" arm at the accessor boundary and
17276 // the [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
17277 // coherence gate would silently accept a struct-literal
17278 // `Caixa` carrying the drift.
17279 //
17280 // Peer of the sibling
17281 // `declared_servico_slots_upgrade_from_arm_routes_through_accessor`
17282 // (2a1f907),
17283 // `declared_supervisor_slots_children_arm_routes_through_accessor`
17284 // (c17b51e), and
17285 // `declared_mesh_slots_membros_arm_routes_through_accessor`
17286 // (0f26987) composition pins on the M2 `:upgrade-from` /
17287 // `:children` / M3 `:membros` composite-slice arms — same "the
17288 // enumerator gate must route through the substrate-primitive
17289 // typed dispatch" discipline extended onto the M3 `:contratos`
17290 // composite-slice arm, closing the M3 mesh-slot arm of the
17291 // declared-slot enumerator's routing invariant on the
17292 // composite-slice inputs.
17293 let c = caixa_aplicacao_with_contratos(vec![contrato_http_for_test(
17294 "cart", "catalog", "/items",
17295 )]);
17296 let slots = c.declared_mesh_slots();
17297 assert!(
17298 slots.contains(&crate::render::M3_AUTHOR_KEY_CONTRATOS),
17299 "declared_mesh_slots must push M3_AUTHOR_KEY_CONTRATOS when \
17300 `:contratos` is non-empty — the accessor and the enumerator \
17301 gate must route through the same substrate-primitive \
17302 typed dispatch on the outer :contratos presence bit (got \
17303 slots={slots:?})",
17304 );
17305 let c = caixa_aplicacao_with_contratos(vec![]);
17306 let slots = c.declared_mesh_slots();
17307 assert!(
17308 !slots.contains(&crate::render::M3_AUTHOR_KEY_CONTRATOS),
17309 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_CONTRATOS \
17310 when `:contratos` is empty — the author-omitted arm must \
17311 route through the accessor's empty-slice return unchanged \
17312 (got slots={slots:?})",
17313 );
17314 }
17315
17316 #[test]
17317 fn aplicacao_view_contratos_arm_routes_through_accessor() {
17318 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:contratos`
17319 // fold-in arm must key off [`Caixa::contratos`], not the raw
17320 // `self.contratos.clone()` field-clone. Structurally: a `Caixa
17321 // { kind: Aplicacao, contratos: vec![WitContract { de: "cart",
17322 // .. }, WitContract { de: "pricing", .. }], .. }` must fold the
17323 // per-edge list through the accessor into the typed
17324 // [`crate::AplicacaoSpec`] view's `contratos` slot verbatim —
17325 // every entry the accessor surfaces must land in the view's
17326 // `contratos` slot in the same order. The pair jointly pins
17327 // the accessor + view-composer composition: a future silent
17328 // detour that had the accessor shuffle or drop an edge would
17329 // silently split the paired declared-slot enumerator's
17330 // presence bit from the typed-view composer's edge-list, a
17331 // two-consumer split at the enumerator and the view composer
17332 // far from the source `caixa.lisp`.
17333 //
17334 // Peer of the sibling
17335 // `aplicacao_view_membros_arm_routes_through_accessor`
17336 // (0f26987) composition pin on the M3 `:membros` outer-
17337 // `&[Composite]` composite-slice arm, closing the aplicacao-
17338 // view composer's routing invariant on the composite-slice
17339 // inputs at the outer altitude.
17340 let c = caixa_aplicacao_with_contratos(vec![
17341 contrato_http_for_test("cart", "catalog", "/items"),
17342 contrato_http_for_test("cart", "pricing", "/price"),
17343 ]);
17344 let view = c
17345 .aplicacao_view()
17346 .expect("Aplicacao kind must produce an aplicacao_view");
17347 assert_eq!(
17348 view.contratos(),
17349 c.contratos(),
17350 "aplicacao_view must fold Caixa::contratos verbatim into \
17351 AplicacaoSpec::contratos — the accessor and the view \
17352 composer must route through the same substrate-primitive \
17353 typed dispatch on the outer :contratos slice (got view \
17354 contratos={:?}, expected {:?})",
17355 view.contratos(),
17356 c.contratos(),
17357 );
17358 }
17359
17360 #[test]
17361 fn contratos_projects_slice_by_borrow() {
17362 // The by-borrow pin: [`Caixa::contratos`] returns `&[WitContract]`
17363 // by borrow — the returned slice borrows the underlying
17364 // `Vec<WitContract>` storage of the `:contratos` slot and the
17365 // accessor must not clone the backing `Vec` on every call.
17366 // Peer of the sibling outer top-level [`Caixa`] `&[T]`-return
17367 // by-borrow pins (`autores_projects_slice_by_borrow` b5d813f,
17368 // `etiquetas_projects_slice_by_borrow` 78c7d3c,
17369 // `bibliotecas_projects_slice_by_borrow` 8a36c23,
17370 // `exe_projects_slice_by_borrow` 65d9527,
17371 // `servicos_projects_slice_by_borrow` 611f78b,
17372 // `deps_projects_slice_by_borrow` ad34b4e,
17373 // `deps_dev_projects_slice_by_borrow` f7fd81e,
17374 // `upgrade_from_projects_slice_by_borrow` 2a1f907,
17375 // `children_projects_slice_by_borrow` c17b51e,
17376 // `membros_projects_slice_by_borrow` 0f26987) on the sibling
17377 // outer top-level [`Caixa`] scalar-element and composite-
17378 // element `&[T]` axes — closes the outer-`Caixa` M3 mesh-slot
17379 // composite-element `&[Composite]` axis on the by-borrow pin:
17380 // the accessor's returned slice must borrow from `&self` (the
17381 // returned reference's lifetime is tied to `&self`), and
17382 // calling the accessor twice on the same [`Caixa`] must yield
17383 // slices that are pointer-equal (the underlying byte-buffer is
17384 // the storage `Vec`'s allocation, not a fresh copy) as well as
17385 // value-equal (idempotent, no side effects on `&self`).
17386 //
17387 // Pins against a future silent detour that returned an owned
17388 // `Vec<WitContract>` (which would type-check but silently clone
17389 // on every call), a `&Vec<WitContract>` return (which would
17390 // leak the backing `Vec`'s grow/push/reserve surface no
17391 // downstream consumer reaches for), or a one-arm-only accessor
17392 // that returned a saturating value on some sentinel input.
17393 for contratos in [
17394 vec![],
17395 vec![contrato_http_for_test("cart", "catalog", "/items")],
17396 vec![
17397 contrato_http_for_test("cart", "catalog", "/items"),
17398 contrato_http_for_test("cart", "pricing", "/price"),
17399 ],
17400 ] {
17401 let c = caixa_aplicacao_with_contratos(contratos.clone());
17402 let first = c.contratos();
17403 let second = c.contratos();
17404 assert_eq!(
17405 first, second,
17406 "Caixa::contratos must be idempotent — two successive \
17407 calls on the same &self must return the same \
17408 &[WitContract]",
17409 );
17410 assert_eq!(
17411 first.as_ptr(),
17412 second.as_ptr(),
17413 "Caixa::contratos must borrow the underlying \
17414 Vec<WitContract> storage — two successive calls must \
17415 return slices with the same backing pointer (a fresh \
17416 Vec<WitContract> clone would change the pointer on \
17417 every call)",
17418 );
17419 assert_eq!(
17420 first,
17421 contratos.as_slice(),
17422 "Caixa::contratos must return :contratos verbatim by \
17423 borrow — got {first:?}, expected {contratos:?}",
17424 );
17425 }
17426 }
17427
17428 // ── drift-detection: Caixa top-level multi-word serde-derive-to-const identity ──
17429
17430 #[test]
17431 fn caixa_multi_word_serde_keys_match_lifted_top_level_key_consts() {
17432 // Load-bearing invariant: every multi-word top-level [`Caixa`]
17433 // serde-derived JSON key routes through a lifted `&'static str`
17434 // const. The Rust field names are `snake_case`
17435 // (`deps_dev` / `upgrade_from` / `max_restarts` /
17436 // `restart_window`); [`Caixa`]'s `#[serde(rename_all =
17437 // "camelCase")]` derive attribute maps each to the camelCase
17438 // byte-string the [`Caixa::to_lisp`] round-trip's
17439 // `serde_json::to_value(self)` step lands under before
17440 // `tatara_lisp::domain::json_to_sexp` re-projects the JSON keys
17441 // to the kebab-case `:deps-dev` / `:upgrade-from` /
17442 // `:max-restarts` / `:restart-window` author surface. Serialize
17443 // a fully-populated [`Caixa`] and pin that each canonical
17444 // byte-sequence appears verbatim in the JSON — a future
17445 // accidental `rename_all = "snake_case"` / `"kebab-case"` /
17446 // verbatim-field-name flip at the derive attribute (any of
17447 // which would silently break every [`Caixa::to_lisp`]
17448 // round-trip and the future M4 operator-side manifest ingest's
17449 // `Value::get(<key>)` navigation) surfaces here as a build-time
17450 // test failure at `manifest.rs`, not as an apply-time
17451 // `.get(<stale-canonical-const>)` returning `None` far from the
17452 // derive-attr drift's commit. Same discipline the sibling
17453 // `supervisor_spec_serde_keys_match_lifted_supervisor_key_consts`
17454 // (40cc4e5), `membro_serde_keys_match_lifted_membro_key_consts`
17455 // (ce80ca0), and `upgrade_from_entry_serde_keys_match_lifted_
17456 // m2_upgrade_from_key_consts` (36ffe65) pins established on the
17457 // sibling M2 supervision-tree, M3 [`Membro`] per-entry, and M2
17458 // [`UpgradeFromEntry`] per-entry axes — extended here to the
17459 // enclosing M0 [`Caixa`] top-level axis so the last of the four
17460 // multi-word top-level [`Caixa`] serde-derived JSON keys
17461 // (`depsDev`) joins the substrate's "one canonical byte-string
17462 // per typed serialized-key axis" discipline.
17463 use crate::supervisor::{ChildSpec, RestartPolicy, RestartStrategy};
17464 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
17465 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
17466 c.deps_dev = vec![Dep::simple("tatara-check", "^0.1")];
17467 c.upgrade_from = vec![UpgradeFromEntry {
17468 from: "0.0.1".into(),
17469 instructions: vec![UpgradeInstruction::Restart],
17470 }];
17471 c.estrategia = Some(RestartStrategy::OneForOne);
17472 c.max_restarts = Some(3);
17473 c.restart_window = Some("60s".into());
17474 c.children = vec![ChildSpec {
17475 caixa: "child".into(),
17476 versao: "^0.1".into(),
17477 restart: RestartPolicy::Permanent,
17478 }];
17479 let json = serde_json::to_string(&c).unwrap();
17480 for key in [
17481 crate::render::CAIXA_KEY_DEPS_DEV,
17482 crate::render::M2_KEY_UPGRADE_FROM,
17483 crate::render::SUPERVISOR_KEY_MAX_RESTARTS,
17484 crate::render::SUPERVISOR_KEY_RESTART_WINDOW,
17485 ] {
17486 let quoted = format!("\"{key}\"");
17487 assert!(
17488 json.contains("ed),
17489 "serialized Caixa must carry the lifted top-level \
17490 multi-word byte-sequence {quoted} verbatim in the JSON \
17491 emission (got: {json})",
17492 );
17493 }
17494 }
17495
17496 #[test]
17497 fn caixa_top_level_multi_word_key_consts_are_pairwise_distinct() {
17498 // Cross-axis drift-detection pin: a future collapse of the four
17499 // canonical [`Caixa`] top-level multi-word byte-strings onto the
17500 // same value (e.g. an accidental copy-paste flip of
17501 // [`crate::render::CAIXA_KEY_DEPS_DEV`] to also read
17502 // `"upgradeFrom"`) would silently reroute every downstream
17503 // `Value::get(<key>)` probe on one axis onto the sibling axis's
17504 // top-level entry and pass every propagation-probe test that
17505 // expected only the stale axis's value. Peer of the sibling
17506 // four-way distinct pin on the `SUPERVISOR_KEY_*` tetrad
17507 // (40cc4e5) and the two-way pin on `MEMBRO_KEY_*` (ce80ca0).
17508 let all = [
17509 crate::render::CAIXA_KEY_DEPS_DEV,
17510 crate::render::M2_KEY_UPGRADE_FROM,
17511 crate::render::SUPERVISOR_KEY_MAX_RESTARTS,
17512 crate::render::SUPERVISOR_KEY_RESTART_WINDOW,
17513 ];
17514 for (i, a) in all.iter().enumerate() {
17515 for b in all.iter().skip(i + 1) {
17516 assert_ne!(
17517 a, b,
17518 "Caixa top-level multi-word key consts must be \
17519 pairwise-distinct canonical byte-sequences — got \
17520 `{a}` == `{b}`",
17521 );
17522 }
17523 }
17524 }
17525
17526 #[test]
17527 fn caixa_top_level_multi_word_key_consts_are_lower_camel_case_shape() {
17528 // Shape-pin: every [`Caixa`] top-level multi-word key const must
17529 // be a lowerCamelCase byte-sequence (no `snake_case`
17530 // underscores, no `kebab-case` hyphens, no leading colon, no
17531 // `PascalCase` leading capital, no whitespace / dots) — the
17532 // canonical shape the `#[serde(rename_all = "camelCase")]`
17533 // derive produces on [`Caixa`]. A future flip to a
17534 // non-camelCase attribute at the derive surfaces both here
17535 // (this test fails on the stale-constant shape) and at
17536 // `caixa_multi_word_serde_keys_match_lifted_top_level_key_consts`
17537 // (that test fails on the mismatch between const and derive).
17538 // Peer with `membro_key_consts_are_lower_camel_case_shape`
17539 // (ce80ca0) and `supervisor_key_consts_are_lower_camel_case_shape`
17540 // (40cc4e5) on the sibling per-entry / supervisor-tree axes.
17541 for key in [
17542 crate::render::CAIXA_KEY_DEPS_DEV,
17543 crate::render::M2_KEY_UPGRADE_FROM,
17544 crate::render::SUPERVISOR_KEY_MAX_RESTARTS,
17545 crate::render::SUPERVISOR_KEY_RESTART_WINDOW,
17546 ] {
17547 assert!(
17548 !key.is_empty(),
17549 "Caixa top-level multi-word key const must be non-empty \
17550 (got {key:?})"
17551 );
17552 let first = key.chars().next().unwrap();
17553 assert!(
17554 first.is_ascii_lowercase(),
17555 "Caixa top-level multi-word key const must lead with an \
17556 ASCII-lowercase byte (got {key:?}, leads with {first:?})",
17557 );
17558 assert!(
17559 key.chars().all(|c| c.is_ascii_alphanumeric()),
17560 "Caixa top-level multi-word key const must be \
17561 ASCII-alphanumeric only — no `_` / `-` / `:` / `.` / \
17562 whitespace (got {key:?})",
17563 );
17564 }
17565 }
17566
17567 #[test]
17568 fn caixa_key_deps_dev_pins_canonical_camel_case_byte_string() {
17569 // Scalar-value pin: the byte-string the
17570 // [`crate::render::CAIXA_KEY_DEPS_DEV`] const resolves to,
17571 // asserted verbatim. A future rebrand (`depsDev` → `devDeps`
17572 // matching Cargo's verbatim `dev-dependencies` axis, `depsDev`
17573 // → `depsTest` matching a hypothetical per-test-target
17574 // vocabulary flip) lands as an edit to exactly one const AND
17575 // one derive attribute — the sibling
17576 // `caixa_multi_word_serde_keys_match_lifted_top_level_key_consts`
17577 // pin already ties the const to the derive attribute, so a
17578 // rebrand that touches only one side of the pair fails at
17579 // caixa-core build time. Same "scalar-value pin per const"
17580 // discipline the sibling
17581 // `m2_top_level_author_key_consts_pin_canonical_kebab_case_labels`
17582 // (f49c8b0) and `contrato_key_consts_pin_canonical_camel_case_labels`
17583 // (ca463a4) pins carry on the peer M2 / M3 top-level slot axes.
17584 assert_eq!(crate::render::CAIXA_KEY_DEPS_DEV, "depsDev");
17585 }
17586
17587 #[test]
17588 fn caixa_key_deps_pins_canonical_byte_string() {
17589 // Scalar-value pin: the byte-string the
17590 // [`crate::render::CAIXA_KEY_DEPS`] const resolves to, asserted
17591 // verbatim. Peer of `caixa_key_deps_dev_pins_canonical_camel_case_byte_string`
17592 // on the two-list dep-graph serialized-key axis — the sibling
17593 // pin covers the multi-word `deps_dev → depsDev` camelCase
17594 // arm, this pin covers the single-word `deps → deps` no-op arm
17595 // (the [`crate::Caixa::deps`] field name carries no `_`, so the
17596 // `#[serde(rename_all = "camelCase")]` derive is a no-op on this
17597 // axis and the emitted JSON key equals the source-side field
17598 // name byte-for-byte). A future [`crate::Caixa::deps`] field
17599 // rename (`deps` → `dependencies` matching Cargo's verbatim
17600 // `[dependencies]` axis, `deps` → `runtime_deps` matching a
17601 // hypothetical per-runtime-target vocabulary flip) OR an added
17602 // `#[serde(rename = "…")]` explicit override lands as an edit
17603 // to exactly one const AND one derive-attr / field name — the
17604 // sibling `caixa_deps_serde_key_matches_lifted_caixa_key_deps`
17605 // pin ties the const to the emitted JSON key, so a rebrand
17606 // that touches only one side of the pair fails at caixa-core
17607 // build time.
17608 assert_eq!(crate::render::CAIXA_KEY_DEPS, "deps");
17609 }
17610
17611 #[test]
17612 fn caixa_deps_serde_key_matches_lifted_caixa_key_deps() {
17613 // Load-bearing invariant on the single-word `deps` top-level
17614 // axis: the byte-string [`crate::render::CAIXA_KEY_DEPS`] pins
17615 // must appear verbatim in the JSON [`Caixa::to_lisp`]'s
17616 // `serde_json::to_value(self)` step emits. Serialize a
17617 // populated [`Caixa`] whose `:deps` slot carries at least one
17618 // entry (the `#[serde(default)]` attribute on the field emits
17619 // an empty `[]` even without members, but a non-empty vec
17620 // additionally covers the codec's per-`Dep`-entry emission
17621 // path) and pin that `"deps"` appears verbatim in the JSON
17622 // emission — a future accidental `rename_all = "snake_case"` /
17623 // `"kebab-case"` flip at the derive attribute (or an added
17624 // `#[serde(rename = "…")]` explicit override on the field, or
17625 // a Rust field rename) would break every [`Caixa::to_lisp`]
17626 // round-trip and the future M4 operator-side manifest ingest's
17627 // `Value::get(CAIXA_KEY_DEPS)` navigation — surfaces here as a
17628 // build-time test failure at `manifest.rs`, not as an
17629 // apply-time `.get(<stale-canonical-const>)` returning `None`
17630 // far from the drift's commit. Peer of the sibling
17631 // `caixa_multi_word_serde_keys_match_lifted_top_level_key_consts`
17632 // multi-word pin on the same M0 [`Caixa`] top-level
17633 // serialized-key axis, extended here to the single-word arm
17634 // the multi-word test's `rename_all = "camelCase"` sweep can't
17635 // reach (single-word `deps → deps` is a no-op the multi-word
17636 // pin's `\"depsDev\"` / `\"upgradeFrom\"` / `\"maxRestarts\"` /
17637 // `\"restartWindow\"` byte-scan can never observe).
17638 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
17639 c.deps = vec![Dep::simple("caixa-core", "^0.1")];
17640 let json = serde_json::to_string(&c).unwrap();
17641 let quoted = format!("\"{}\"", crate::render::CAIXA_KEY_DEPS);
17642 assert!(
17643 json.contains("ed),
17644 "serialized Caixa must carry the lifted top-level `deps` \
17645 byte-sequence {quoted} verbatim in the JSON emission (got: \
17646 {json})",
17647 );
17648 }
17649
17650 #[test]
17651 fn caixa_dep_graph_two_list_key_consts_are_pairwise_distinct() {
17652 // Cross-axis drift-detection pin on the two-list dep-graph
17653 // renderer-side wire-key axis: a future collapse of the
17654 // canonical [`crate::render::CAIXA_KEY_DEPS`] /
17655 // [`crate::render::CAIXA_KEY_DEPS_DEV`] byte-strings onto the
17656 // same value (e.g. an accidental copy-paste flip of
17657 // `CAIXA_KEY_DEPS_DEV` to also read `"deps"`) would silently
17658 // reroute every downstream `Value::get(<key>)` probe on one
17659 // axis onto the sibling axis's dep-list and pass every
17660 // propagation-probe test that expected only the stale axis's
17661 // value — a dev-only dep would land in the runtime closure at
17662 // publish time, or a runtime dep would be excluded from the
17663 // published lacre. Peer of the sibling four-way distinct pin
17664 // on the top-level multi-word tetrad
17665 // (`caixa_top_level_multi_word_key_consts_are_pairwise_distinct`)
17666 // and the two-way pin on the sibling
17667 // [`DEP_AUTHOR_KEY_DEPS`] / [`DEP_AUTHOR_KEY_DEPS_DEV`]
17668 // author-facing arm (4da6fba's test), extended here to the
17669 // renderer-side wire-key arm of the same two-list dep-graph
17670 // axis so both halves of the "one canonical byte-string per
17671 // typed axis per (author, wire)" grid carry the same
17672 // distinct-ness discipline.
17673 assert_ne!(
17674 crate::render::CAIXA_KEY_DEPS,
17675 crate::render::CAIXA_KEY_DEPS_DEV,
17676 "CAIXA_KEY_DEPS and CAIXA_KEY_DEPS_DEV must be distinct \
17677 canonical byte-sequences on the two-list dep-graph \
17678 renderer-side wire-key axis"
17679 );
17680 }
17681
17682 // ── DepList / Caixa::push_dep pin ────────────────────────────────
17683 //
17684 // The compounding pin: the two-arm closed-set typed enum
17685 // [`crate::dep::DepList`] carries the runtime-closure `:deps`
17686 // (`Prod`) vs dev-only-closure `:deps-dev` (`Dev`) dispatch every
17687 // consumer of the top-level manifest's dep-mutation surface reads
17688 // through, and the typed dispatch [`Caixa::push_dep`] on the
17689 // substrate primitive folds the "select list → check within-list
17690 // dup → push" cascade onto one method call. Prior to this landing
17691 // the two axes lived across two `&'static str` constants
17692 // (`DEP_AUTHOR_KEY_DEPS`, `DEP_AUTHOR_KEY_DEPS_DEV`) with no closed-
17693 // set type carrying the pair; the `feira add` mutation site's
17694 // inline `if self.dev { &mut caixa.deps_dev } else { &mut
17695 // caixa.deps }` dispatch expressed no compile-time link back to
17696 // the substrate primitive, and a future third dep-list axis would
17697 // have silently split at every open-coded mutation site.
17698
17699 #[test]
17700 fn dep_list_as_str_routes_through_lifted_author_key_constants() {
17701 // Every arm returns the same `&'static str` the substrate's
17702 // canonical `DEP_AUTHOR_KEY_DEPS` / `DEP_AUTHOR_KEY_DEPS_DEV`
17703 // constants carry. A future rebrand on either constant reaches
17704 // the enum through one edit; a regression to inline literals
17705 // (e.g. `Prod => ":deps"`) would silently split the diagnostic
17706 // quotes from the wire-format constants every consumer routes
17707 // through and this pin flags it at build time.
17708 assert_eq!(
17709 crate::dep::DepList::Prod.as_str(),
17710 crate::render::DEP_AUTHOR_KEY_DEPS
17711 );
17712 assert_eq!(
17713 crate::dep::DepList::Dev.as_str(),
17714 crate::render::DEP_AUTHOR_KEY_DEPS_DEV
17715 );
17716 }
17717
17718 #[test]
17719 fn dep_list_display_routes_through_as_str() {
17720 // Same as-str-through-Display convergence discipline the
17721 // sibling closed-set typed enums carry — a `format!("{list}")`
17722 // call must land byte-for-byte on the accessor's return so a
17723 // future consumer that formats the enum for a diagnostic line
17724 // reaches the same wire-format constant the wire-format
17725 // producers do.
17726 assert_eq!(
17727 format!("{}", crate::dep::DepList::Prod),
17728 crate::dep::DepList::Prod.as_str()
17729 );
17730 assert_eq!(
17731 format!("{}", crate::dep::DepList::Dev),
17732 crate::dep::DepList::Dev.as_str()
17733 );
17734 }
17735
17736 #[test]
17737 fn dep_list_all_enumerates_every_variant_once() {
17738 // Exhaustive-iteration pin — every arm appears exactly once in
17739 // `ALL`, matching the closed set the compiler enforces on the
17740 // sibling `match self` arms. A future variant addition that
17741 // extends only one method's match without extending `ALL`
17742 // would silently drop the new arm from every consumer that
17743 // iterates the slice.
17744 let variants: &[crate::dep::DepList] = crate::dep::DepList::ALL;
17745 assert!(variants.contains(&crate::dep::DepList::Prod));
17746 assert!(variants.contains(&crate::dep::DepList::Dev));
17747 assert_eq!(variants.len(), 2);
17748 }
17749
17750 #[test]
17751 fn dep_list_from_wire_returns_prod_on_deps_wire_scalar() {
17752 // Reverse projection on the two-list dep-graph axis: the
17753 // author-surface wire tag the sibling `as_str` emitter walks
17754 // for `Prod` (`:deps` via `DEP_AUTHOR_KEY_DEPS`) parses back to
17755 // `Some(DepList::Prod)`. A regression that hand-rolled the
17756 // per-arm match without routing through the lifted
17757 // `DEP_AUTHOR_KEY_DEPS` const would silently disagree on any
17758 // future wire-tag rebrand and this pin flags it at build time.
17759 assert_eq!(
17760 crate::dep::DepList::from_wire(crate::render::DEP_AUTHOR_KEY_DEPS),
17761 Some(crate::dep::DepList::Prod)
17762 );
17763 }
17764
17765 #[test]
17766 fn dep_list_from_wire_returns_dev_on_deps_dev_wire_scalar() {
17767 // Peer of the `Prod`-arm pin on the dev-only axis: the
17768 // author-surface wire tag the sibling `as_str` emitter walks
17769 // for `Dev` (`:deps-dev` via `DEP_AUTHOR_KEY_DEPS_DEV`) parses
17770 // back to `Some(DepList::Dev)`. Same drift-detection posture
17771 // as the peer arm — the sibling method `match` arms are
17772 // compiler-checked exhaustive so a future variant addition
17773 // trips at build time.
17774 assert_eq!(
17775 crate::dep::DepList::from_wire(crate::render::DEP_AUTHOR_KEY_DEPS_DEV),
17776 Some(crate::dep::DepList::Dev)
17777 );
17778 }
17779
17780 #[test]
17781 fn dep_list_from_wire_returns_none_on_unknown_wire_scalar() {
17782 // Every input outside the closed-set arm-string set the
17783 // sibling `as_str` emitter walks lands on the terminal `None`
17784 // fallback — no silent-accept surface. Sweeps a set of
17785 // plausibly-adjacent scalars (unprefixed wire form, PascalCase
17786 // rebrand candidates, foreign wire tags, empty string) so a
17787 // future variant addition that widened one wire form without
17788 // extending the emitter's arm-set would trip the sibling
17789 // round-trip pin below rather than silently accepting the new
17790 // form here.
17791 for candidate in [
17792 "",
17793 "deps",
17794 "deps-dev",
17795 ":deps ",
17796 ":Deps",
17797 ":DEPS",
17798 ":build-dep",
17799 ":tool-dep",
17800 "prod",
17801 "dev",
17802 ] {
17803 assert_eq!(
17804 crate::dep::DepList::from_wire(candidate),
17805 None,
17806 "from_wire({candidate:?}) must return None; every input outside \
17807 the {{DEP_AUTHOR_KEY_DEPS, DEP_AUTHOR_KEY_DEPS_DEV}} accept-set \
17808 the sibling as_str emitter walks lands on the terminal fallback",
17809 );
17810 }
17811 }
17812
17813 #[test]
17814 fn dep_list_round_trips_through_as_str_and_from_wire() {
17815 // Load-bearing round-trip pin: every arm the `ALL` iteration
17816 // exposes survives the `as_str` → `from_wire` composition
17817 // byte-for-byte. Same discipline the sibling closed-set enums
17818 // carry — `CaixaKind` /
17819 // `RestartStrategy` / `RestartPolicy` /
17820 // `PlacementStrategy` — extended onto the two-list dep-graph
17821 // axis. A future variant addition that extends `ALL` +
17822 // `as_str` without extending `from_wire` (or vice versa)
17823 // trips at build time on this iteration because the compiler
17824 // enforces exhaustiveness on the sibling `match self` arms.
17825 for &list in crate::dep::DepList::ALL {
17826 assert_eq!(
17827 crate::dep::DepList::from_wire(list.as_str()),
17828 Some(list),
17829 "DepList::from_wire(as_str({list:?})) must round-trip to Some({list:?}) — \
17830 a silent split between the forward emitter and the reverse parser \
17831 would drift the two halves of the two-list dep-graph axis's typed dispatch",
17832 );
17833 }
17834 }
17835
17836 #[test]
17837 fn push_dep_routes_to_deps_slot_on_prod_arm() {
17838 // The `Prod` arm dispatches to the runtime-closure `:deps`
17839 // slot every downstream lacre-pipeline consumer resolves at
17840 // build time. A future arm that regressed to inline `&mut
17841 // self.deps_dev` on the `Prod` path would silently reroute
17842 // every runtime dep into the dev-only closure at publish time
17843 // — this pin refuses that regression.
17844 let src = Caixa::template("host");
17845 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17846 let before_deps = caixa.deps().len();
17847 let before_deps_dev = caixa.deps_dev().len();
17848 let dep = Dep {
17849 nome: "caixa-teia".to_string(),
17850 versao: "^0.1".to_string(),
17851 fonte: None,
17852 opcional: false,
17853 caracteristicas: Vec::new(),
17854 };
17855 caixa
17856 .push_dep(crate::dep::DepList::Prod, dep)
17857 .expect("first push into :deps succeeds");
17858 assert_eq!(caixa.deps().len(), before_deps + 1);
17859 assert_eq!(caixa.deps_dev().len(), before_deps_dev);
17860 assert_eq!(caixa.deps().last().unwrap().nome(), "caixa-teia");
17861 }
17862
17863 #[test]
17864 fn push_dep_routes_to_deps_dev_slot_on_dev_arm() {
17865 // Peer of the sibling `Prod`-arm dispatch pin — the `Dev` arm
17866 // must dispatch to the dev-only-closure `:deps-dev` slot every
17867 // downstream test-facing artifact resolver reads. A future
17868 // regression that inverted the two arms would silently route
17869 // every dev-only dep into the runtime closure at publish time
17870 // and this pin catches it before the drift ships.
17871 let src = Caixa::template("host");
17872 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17873 let dep = Dep {
17874 nome: "tatara-check".to_string(),
17875 versao: "*".to_string(),
17876 fonte: None,
17877 opcional: false,
17878 caracteristicas: Vec::new(),
17879 };
17880 caixa
17881 .push_dep(crate::dep::DepList::Dev, dep)
17882 .expect("first push into :deps-dev succeeds");
17883 assert!(caixa.deps().is_empty());
17884 assert_eq!(caixa.deps_dev().len(), 1);
17885 assert_eq!(caixa.deps_dev().last().unwrap().nome(), "tatara-check");
17886 }
17887
17888 #[test]
17889 fn push_dep_refuses_within_list_duplicate_nome_with_typed_error() {
17890 // Within-list dup check routes through the canonical
17891 // [`DepError::DuplicateNome`] carrier — the substrate's typed
17892 // diagnostic for the same axis [`Caixa::validate_deps`]'s
17893 // parse-time [`crate::render::insert_first_seen`] walk raises
17894 // on. Prior to the lift the mutation site's inline
17895 // `bail!("dep '{}' already declared", …)` string-diagnostic
17896 // path expressed no through-line back to the typed error;
17897 // routing every dep-list refusal through one carrier means an
17898 // author reading a `feira add` refusal and a `feira build`
17899 // refusal reaches for the same corrective surface without
17900 // switching diagnostic idioms.
17901 let src = Caixa::template("host");
17902 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17903 let dep = Dep {
17904 nome: "caixa-teia".to_string(),
17905 versao: "^0.1".to_string(),
17906 fonte: None,
17907 opcional: false,
17908 caracteristicas: Vec::new(),
17909 };
17910 caixa
17911 .push_dep(crate::dep::DepList::Prod, dep.clone())
17912 .expect("first push succeeds");
17913 let dup = Dep {
17914 nome: "caixa-teia".to_string(),
17915 versao: "^0.2".to_string(),
17916 fonte: None,
17917 opcional: false,
17918 caracteristicas: Vec::new(),
17919 };
17920 let err = caixa
17921 .push_dep(crate::dep::DepList::Prod, dup)
17922 .expect_err("second push with same :nome refuses");
17923 assert_eq!(
17924 err,
17925 DepError::DuplicateNome {
17926 nome: "caixa-teia".to_string(),
17927 list: crate::render::DEP_AUTHOR_KEY_DEPS,
17928 }
17929 );
17930 // The refused mutation must not corrupt the target list —
17931 // exactly one entry lives past the refusal, matching the
17932 // canonical single-source-of-truth invariant `Caixa::deps()`
17933 // carries.
17934 assert_eq!(caixa.deps().len(), 1);
17935 }
17936
17937 #[test]
17938 fn push_dep_refuses_dup_on_dev_list_arm_names_deps_dev_key() {
17939 // Peer of the sibling `Prod`-arm dup-refusal pin — the `Dev`
17940 // arm's refusal must carry `DEP_AUTHOR_KEY_DEPS_DEV` in the
17941 // `list` payload so a future author reading the refusal grep's
17942 // for the correct `:deps-dev` block in their `caixa.lisp`,
17943 // not the sibling `:deps` block the runtime closure resolves.
17944 let src = Caixa::template("host");
17945 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17946 let dep = Dep {
17947 nome: "tatara-check".to_string(),
17948 versao: "*".to_string(),
17949 fonte: None,
17950 opcional: false,
17951 caracteristicas: Vec::new(),
17952 };
17953 caixa
17954 .push_dep(crate::dep::DepList::Dev, dep.clone())
17955 .expect("first push succeeds");
17956 let err = caixa
17957 .push_dep(crate::dep::DepList::Dev, dep)
17958 .expect_err("second push with same :nome refuses");
17959 assert!(matches!(
17960 err,
17961 DepError::DuplicateNome {
17962 ref nome,
17963 list,
17964 } if nome == "tatara-check"
17965 && list == crate::render::DEP_AUTHOR_KEY_DEPS_DEV
17966 ));
17967 }
17968
17969 #[test]
17970 fn push_dep_allows_same_nome_across_prod_and_dev_lists() {
17971 // The within-list dup check is scoped to the target arm — a
17972 // caixa may legitimately carry the same `:nome` under both
17973 // `:deps` and `:deps-dev` (though the substrate's peer
17974 // [`crate::Caixa::validate_deps`] walk still refuses the
17975 // shape at parse time; the mutation-site refusal is scoped to
17976 // the mutation-site's list to match the peer parse-time
17977 // per-list [`crate::render::insert_first_seen`] discipline).
17978 // The two arms hold independent seen-sets.
17979 let src = Caixa::template("host");
17980 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17981 let dep_prod = Dep {
17982 nome: "shared".to_string(),
17983 versao: "^0.1".to_string(),
17984 fonte: None,
17985 opcional: false,
17986 caracteristicas: Vec::new(),
17987 };
17988 let dep_dev = Dep {
17989 nome: "shared".to_string(),
17990 versao: "*".to_string(),
17991 fonte: None,
17992 opcional: false,
17993 caracteristicas: Vec::new(),
17994 };
17995 caixa
17996 .push_dep(crate::dep::DepList::Prod, dep_prod)
17997 .expect("push into :deps succeeds");
17998 caixa
17999 .push_dep(crate::dep::DepList::Dev, dep_dev)
18000 .expect("push same :nome into :deps-dev succeeds");
18001 assert_eq!(caixa.deps().len(), 1);
18002 assert_eq!(caixa.deps_dev().len(), 1);
18003 }
18004
18005 #[test]
18006 fn deps_of_prod_returns_the_deps_slot_verbatim() {
18007 // The `Prod` arm of the typed-dispatch [`Caixa::deps_of`] read
18008 // accessor must project onto the runtime-closure `:deps` slot —
18009 // element-equal and length-equal to the sibling per-slot
18010 // [`Caixa::deps`] accessor's return over every per-caixa fixture.
18011 // A future arm that regressed to `self.deps_dev()` on the `Prod`
18012 // path would silently reroute every downstream typed-dispatch
18013 // walker (the [`Caixa::validate_deps`] per-list
18014 // [`crate::render::insert_first_seen`] dedup walk, any future
18015 // per-axis-parametrised consumer) into the sibling dev-only
18016 // closure and this pin refuses that regression.
18017 let src = Caixa::template("host");
18018 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
18019 assert_eq!(caixa.deps_of(crate::dep::DepList::Prod), caixa.deps());
18020 assert_eq!(caixa.deps_of(crate::dep::DepList::Prod).len(), 0);
18021 let dep = Dep {
18022 nome: "caixa-teia".to_string(),
18023 versao: "^0.1".to_string(),
18024 fonte: None,
18025 opcional: false,
18026 caracteristicas: Vec::new(),
18027 };
18028 caixa
18029 .push_dep(crate::dep::DepList::Prod, dep.clone())
18030 .expect("push into :deps succeeds");
18031 assert_eq!(caixa.deps_of(crate::dep::DepList::Prod), caixa.deps());
18032 assert_eq!(caixa.deps_of(crate::dep::DepList::Prod).len(), 1);
18033 assert_eq!(
18034 caixa.deps_of(crate::dep::DepList::Prod)[0].nome(),
18035 "caixa-teia"
18036 );
18037 }
18038
18039 #[test]
18040 fn deps_of_dev_returns_the_deps_dev_slot_verbatim() {
18041 // Peer of the sibling `Prod`-arm pin — the `Dev` arm of
18042 // [`Caixa::deps_of`] must project onto the dev-only-closure
18043 // `:deps-dev` slot, element-equal and length-equal to the
18044 // sibling per-slot [`Caixa::deps_dev`] accessor's return. A
18045 // future regression that inverted the two arms would silently
18046 // route every dev-list walker onto the runtime closure and this
18047 // pin catches it before the drift ships.
18048 let src = Caixa::template("host");
18049 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
18050 assert_eq!(caixa.deps_of(crate::dep::DepList::Dev), caixa.deps_dev());
18051 assert_eq!(caixa.deps_of(crate::dep::DepList::Dev).len(), 0);
18052 let dep = Dep {
18053 nome: "tatara-check".to_string(),
18054 versao: "*".to_string(),
18055 fonte: None,
18056 opcional: false,
18057 caracteristicas: Vec::new(),
18058 };
18059 caixa
18060 .push_dep(crate::dep::DepList::Dev, dep)
18061 .expect("push into :deps-dev succeeds");
18062 assert_eq!(caixa.deps_of(crate::dep::DepList::Dev), caixa.deps_dev());
18063 assert_eq!(caixa.deps_of(crate::dep::DepList::Dev).len(), 1);
18064 assert_eq!(
18065 caixa.deps_of(crate::dep::DepList::Dev)[0].nome(),
18066 "tatara-check"
18067 );
18068 }
18069
18070 #[test]
18071 fn deps_of_exhaustive_over_dep_list_all_covers_the_two_slots() {
18072 // Composition pin: iterating [`crate::dep::DepList::ALL`] through
18073 // [`Caixa::deps_of`] must land on the same two-slot partition the
18074 // per-slot [`Caixa::deps`] / [`Caixa::deps_dev`] accessors
18075 // expose — the canonical dispatch a future per-axis-parametrised
18076 // walker (a future `feira app graph` per-list dep summary, a
18077 // future M4 per-cluster dev-closure-audit overlay the CR
18078 // materializer resolves per-CR) reads through. Prior to the
18079 // lift the two-block iteration lived open-coded at every walker,
18080 // so a future third dep-list axis (`:deps-build`, per CAIXA-SDLC
18081 // §I) would have had to grow a third block at every consumer.
18082 // A regression that dropped the `Dev` arm from `ALL` would flip
18083 // the collected pairs to `[(":deps", &[])]` alone and this pin
18084 // refuses that shape.
18085 let src = Caixa::template("host");
18086 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
18087 let prod_dep = Dep {
18088 nome: "caixa-teia".to_string(),
18089 versao: "^0.1".to_string(),
18090 fonte: None,
18091 opcional: false,
18092 caracteristicas: Vec::new(),
18093 };
18094 let dev_dep = Dep {
18095 nome: "tatara-check".to_string(),
18096 versao: "*".to_string(),
18097 fonte: None,
18098 opcional: false,
18099 caracteristicas: Vec::new(),
18100 };
18101 caixa
18102 .push_dep(crate::dep::DepList::Prod, prod_dep)
18103 .expect("push into :deps succeeds");
18104 caixa
18105 .push_dep(crate::dep::DepList::Dev, dev_dep)
18106 .expect("push into :deps-dev succeeds");
18107 let collected: Vec<(&'static str, usize, &str)> = crate::dep::DepList::ALL
18108 .iter()
18109 .map(|&list| {
18110 let slice = caixa.deps_of(list);
18111 (list.as_str(), slice.len(), slice[0].nome())
18112 })
18113 .collect();
18114 assert_eq!(
18115 collected,
18116 vec![
18117 (crate::render::DEP_AUTHOR_KEY_DEPS, 1, "caixa-teia"),
18118 (crate::render::DEP_AUTHOR_KEY_DEPS_DEV, 1, "tatara-check"),
18119 ]
18120 );
18121 }
18122
18123 #[test]
18124 fn validate_deps_iterates_through_dep_list_all_via_deps_of() {
18125 // Composition pin: the [`Caixa::validate_deps`] parse-time gate
18126 // must route its per-list [`crate::render::insert_first_seen`]
18127 // dedup walk through [`Caixa::deps_of`] + [`crate::dep::DepList::ALL`]
18128 // rather than the pre-lift open-coded two-block iteration over
18129 // `self.deps()` + `self.deps_dev()`. A regression that dropped
18130 // one arm (e.g. hand-inlining `self.deps()` alone) would silently
18131 // stop refusing within-list dups on the sibling arm; a
18132 // regression that flipped the arm-to-list-key mapping
18133 // (`Dev => DEP_AUTHOR_KEY_DEPS`) would silently mislabel the
18134 // diagnostic surface. Both drifts surface here through a paired
18135 // duplicate-name refusal per arm plus an offending-list-key
18136 // check on the emitted [`DepError::DuplicateNome`] carrier.
18137 for &list in crate::dep::DepList::ALL {
18138 let src = Caixa::template("host");
18139 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
18140 let dup = Dep {
18141 nome: "twin".to_string(),
18142 versao: "^0.1".to_string(),
18143 fonte: None,
18144 opcional: false,
18145 caracteristicas: Vec::new(),
18146 };
18147 match list {
18148 crate::dep::DepList::Prod => {
18149 caixa.deps.push(dup.clone());
18150 caixa.deps.push(dup);
18151 }
18152 crate::dep::DepList::Dev => {
18153 caixa.deps_dev.push(dup.clone());
18154 caixa.deps_dev.push(dup);
18155 }
18156 }
18157 let err = caixa
18158 .validate_deps()
18159 .expect_err("within-list duplicate :nome must refuse");
18160 assert_eq!(
18161 err,
18162 DepError::DuplicateNome {
18163 nome: "twin".to_string(),
18164 list: list.as_str(),
18165 },
18166 "validate_deps on {list} arm must emit \
18167 DepError::DuplicateNome carrying the arm's own \
18168 as_str() diagnostic — the arm-to-list-key mapping \
18169 flowed through DepList::ALL + Caixa::deps_of"
18170 );
18171 }
18172 }
18173}