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` `:descricao` free-form-prose
540 /// chart-description scalar accessor every consumer of the top-level
541 /// manifest's Chart.yaml `description:` axis keys off — returns the
542 /// author-declared `:descricao` byte-string verbatim as an
543 /// `Option<&str>`, borrowed from the typed slot's own
544 /// `Option<String>` storage. `None` when the slot is absent (the
545 /// canonical "omit to defer to the per-renderer `caixa.nome`-derived
546 /// fallback" shape — [`caixa-helm`]'s `build_chart_yaml` folds the
547 /// omitted slot through a `format!("Generated chart for caixa Servico
548 /// {}", caixa.nome)` fallback, [`caixa-helm`]'s `build_readme` folds
549 /// it through a `format!("caixa Servico {}", caixa.nome)` fallback,
550 /// and [`caixa-feira`]'s `render_flake` folds it through a
551 /// `format!("caixa {}", c.nome)` `flake.nix` `description = ""`
552 /// fallback — each derived from `caixa.nome` on the null-carrier arm).
553 ///
554 /// The `:descricao` slot carries the universal-axis free-form-prose
555 /// chart-description identifier every kind of caixa emits under
556 /// (CAIXA-SDLC §I — the author-facing surface every `defcaixa` form
557 /// supplies) — the typed slot's `Option<String>` accept-set
558 /// (empty-string rejected through [`ManifestError::DescricaoEmpty`],
559 /// chart-description-shape-invalid rejected through
560 /// [`ManifestError::DescricaoInvalid`] past the shared
561 /// [`crate::render::is_chart_description_shape`] predicate the peer
562 /// per-`Caixa` `:descricao` axis also routes through) maps onto four
563 /// load-bearing downstream consumers:
564 ///
565 /// - [`Self::validate_descricao`]'s empty-arm + shape-predicate
566 /// gate binding — the universal-axis identity gate wired at
567 /// caixa-build time.
568 /// - [`caixa-helm`]'s `build_chart_yaml` `ChartYaml.description`
569 /// `Chart.yaml` field fold — the rendered `lareira-<nome>` Helm
570 /// chart's `Chart.yaml` `description:` field, which
571 /// `apiVersion: v2` charts require non-empty (`helm lint` fires
572 /// `WARNING [chart.metadata.description]: description is required`
573 /// when absent) and which every registry that ingests the chart
574 /// (ArtifactHub, chartmuseum, `helm search repo`) surfaces as the
575 /// chart's canonical one-line prose descriptor.
576 /// - [`caixa-helm`]'s `build_readme` chart-`README.md` header fold
577 /// — the rendered `lareira-<nome>` chart's `README.md` prose
578 /// header directly beneath the `# <chart-name>` title, which
579 /// every author who inspects the rendered chart bundle lands at.
580 /// - [`caixa-feira`]'s `render_flake` `flake.nix` `description = ""`
581 /// top-level fold — the emitted `flake.nix`'s `description`
582 /// field, which every Nix consumer (`nix flake show`,
583 /// `nix flake metadata`, downstream flake-registry ingestors)
584 /// surfaces as the flake's canonical descriptor.
585 ///
586 /// Prior to this lift the `.descricao` field was accessed inline at
587 /// four production sites — [`Self::validate_descricao`]'s
588 /// `self.descricao.as_deref()` empty-and-shape gate binding, the
589 /// caixa-helm `build_chart_yaml`
590 /// `caixa.descricao.clone().unwrap_or_else(|| format!(...))`
591 /// `Chart.yaml` `description:` fold, the caixa-helm `build_readme`
592 /// `caixa.descricao.clone().unwrap_or_else(|| format!(...))`
593 /// `README.md` header fold, and the caixa-feira `render_flake`
594 /// `c.descricao.clone().unwrap_or_else(|| format!(...))` `flake.nix`
595 /// `description = ""` fold — four open-coded field-accesses that
596 /// expressed no compile-time link back to the typed slot. A future
597 /// extension of the `:descricao` axis to a richer author surface —
598 /// a per-`:descricao` locale-tagged multi-language descriptor map
599 /// (the "one caixa, N language-tagged prose descriptions" arm
600 /// author-tooling internationalization anticipates), a
601 /// per-registry-target length-and-shape overlay the M4 CR
602 /// materializer resolves per-CR (the "ArtifactHub caps description
603 /// at 512 bytes but the internal registry caps at 256" arm), a
604 /// promotion of the plain `Option<String>` byte-string to a richer
605 /// `ChartDescription` newtype guaranteeing the
606 /// `is_chart_description_shape` predicate at the type level — would
607 /// have had to be threaded through all four open-coded copies in
608 /// lockstep or the validate gate and the three emit paths would
609 /// silently disagree on which prose string a given [`Caixa`]
610 /// resolves to (an author's
611 /// `:descricao "Checkout flow orchestration."` would satisfy
612 /// validate while one of the emit paths silently rendered a stale
613 /// `caixa.nome`-derived fallback, or vice versa). Lifting the
614 /// resolution to a typed method on the substrate primitive means
615 /// every downstream consumer of the caixa's per-`Caixa`
616 /// chart-description surface reaches for exactly one typed dispatch
617 /// — the resolver's accept-set migrates as a unit on any future
618 /// axis addition.
619 ///
620 /// Third outer top-level [`Caixa`] `Option<&str>`-return scalar
621 /// accessor — sibling of [`Self::licenca`] (6d5bc28) and
622 /// [`Self::repositorio`] (cc7332d), the accessors that opened the
623 /// "outer [`Caixa`] `Option<&str>` scalar" projection pattern this
624 /// lift folds on. Same "one typed dispatch on the substrate
625 /// primitive, thin projections at each consumer" discipline the
626 /// peer per-`:placement`
627 /// [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
628 /// [`crate::aplicacao::Placement::affinity`] (74ec2d3) /
629 /// per-`:contratos` [`crate::aplicacao::WitContract::endpoint`]
630 /// (7020470) / [`crate::aplicacao::WitContract::subject`] (90de675)
631 /// / [`crate::aplicacao::WitContract::slot`] (ed22b66)
632 /// `Option<&str>`-return accessors carry on the sibling M2 / M3
633 /// typed-slot atom axes, extended here to the third outer top-level
634 /// `Caixa` universal-axis surface. Named `descricao()` to match the
635 /// storage field's name; the accessor's identity maps onto the
636 /// canonical CAIXA-SDLC §I vocabulary the slot's docstring already
637 /// carries. The one remaining universal `Option<String>` slot
638 /// (`:edicao`) folds on this pattern next.
639 #[must_use]
640 pub fn descricao(&self) -> Option<&str> {
641 self.descricao.as_deref()
642 }
643
644 /// Substrate-canonical per-`Caixa` `:edicao` language-edition scalar
645 /// accessor every consumer of the top-level manifest's tatara-lisp
646 /// edition-selector axis keys off — returns the author-declared
647 /// `:edicao` byte-string verbatim as an `Option<&str>`, borrowed from
648 /// the typed slot's own `Option<String>` storage. `None` when the
649 /// slot is absent (the canonical "omit the slot to defer to the
650 /// substrate's default edition" shape every existing
651 /// [`caixa-resolver`] integration test fixture carries via
652 /// `edicao: None` — see `caixa-resolver/tests/git_integration.rs`;
653 /// the peer [`Self::validate_edicao`] gate is a no-op on the omitted
654 /// arm by construction, so an author-omitted `:edicao` round-trips
655 /// to a build without triggering the year-shape predicate).
656 ///
657 /// The `:edicao` slot carries the universal-axis 4-digit-ASCII-
658 /// decimal-year language-edition identifier every kind of caixa
659 /// emits under (CAIXA-SDLC §I — the author-facing surface every
660 /// `defcaixa` form supplies) — the typed slot's `Option<String>`
661 /// accept-set (empty-string rejected through
662 /// [`ManifestError::EdicaoEmpty`], year-shape-invalid rejected
663 /// through [`ManifestError::EdicaoInvalid`] past the 4-digit-ASCII-
664 /// decimal-year predicate [`Self::validate_edicao`] enforces) maps
665 /// onto one load-bearing downstream consumer today
666 /// ([`Self::validate_edicao`]'s empty-arm + year-shape-predicate
667 /// gate binding at caixa-core/src/manifest.rs:1959) plus every
668 /// future edition-aware substrate consumer the CAIXA-SDLC §I
669 /// roadmap anticipates (the tatara-lisp compiler's macro-surface
670 /// selector every edition-aware build step keys off, the future
671 /// per-edition compatibility-flag overlay the M4 CR materializer
672 /// resolves per-CR, the peer [`Caixa::template`] canonical
673 /// `:edicao "2026"` scaffold every `feira init` emits verbatim,
674 /// and the renderer-side fixtures at `caixa-helm/src/lib.rs:978` /
675 /// `caixa-flux/src/lib.rs:2319` / `caixa-mesh/src/lib.rs:3208` that
676 /// carry `edicao: Some("2026".into())` by construction).
677 ///
678 /// Prior to this lift the `.edicao` field was accessed inline at
679 /// one production site — [`Self::validate_edicao`]'s
680 /// `self.edicao.as_deref()` empty-and-shape gate binding — one
681 /// open-coded field-access that expressed no compile-time link
682 /// back to the typed slot. A future extension of the `:edicao`
683 /// axis to a richer author surface — a per-`:edicao` known-
684 /// edition allowlist (the future tightening
685 /// [`Self::validate_edicao`]'s docstring acknowledges past the
686 /// structural year-shape floor, rejecting year-shaped values that
687 /// don't name a tatara-lisp edition the substrate actually
688 /// understands — `"1999"` is year-shaped but no `1999` edition
689 /// exists), a per-edition compatibility-flag overlay the M4 CR
690 /// materializer resolves per-CR (the "edition `"2026"` enables
691 /// macro-surface features the sibling `"2018"` gates behind a
692 /// feature flag" arm the edition-selector story anticipates), a
693 /// promotion of the plain `Option<String>` byte-string to a
694 /// richer `CaixaEdition` enum discriminated on year once a sibling
695 /// edition to `"2026"` lands — would have had to be threaded
696 /// through the open-coded copy in lockstep with every future
697 /// edition-aware consumer, or the validate gate and the future
698 /// edition-aware consumer path would silently disagree on which
699 /// edition a given [`Caixa`] resolves to (an author's
700 /// `:edicao "2026"` would satisfy validate while a future
701 /// edition-aware consumer silently defaulted to a stale edition,
702 /// or vice versa). Lifting the resolution to a typed method on
703 /// the substrate primitive means every downstream consumer of the
704 /// caixa's per-`Caixa` edition surface reaches for exactly one
705 /// typed dispatch — the resolver's accept-set migrates as a unit
706 /// on any future axis addition.
707 ///
708 /// Fourth and final outer top-level [`Caixa`] `Option<&str>`-return
709 /// scalar accessor — sibling of [`Self::licenca`] (6d5bc28),
710 /// [`Self::repositorio`] (cc7332d), and [`Self::descricao`]
711 /// (3f16e2f), the accessors that opened the "outer [`Caixa`]
712 /// `Option<&str>` scalar" projection pattern this lift folds on.
713 /// Same "one typed dispatch on the substrate primitive, thin
714 /// projections at each consumer" discipline the peer per-`:placement`
715 /// [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
716 /// [`crate::aplicacao::Placement::affinity`] (74ec2d3) /
717 /// per-`:contratos` [`crate::aplicacao::WitContract::endpoint`]
718 /// (7020470) / [`crate::aplicacao::WitContract::subject`] (90de675)
719 /// / [`crate::aplicacao::WitContract::slot`] (ed22b66)
720 /// `Option<&str>`-return accessors carry on the sibling M2 / M3
721 /// typed-slot atom axes, extended here to close the outer top-level
722 /// `Caixa` universal-axis surface's last unlifted `Option<String>`
723 /// slot. Named `edicao()` to match the storage field's name; the
724 /// accessor's identity maps onto the canonical CAIXA-SDLC §I
725 /// vocabulary the slot's docstring already carries.
726 #[must_use]
727 pub fn edicao(&self) -> Option<&str> {
728 self.edicao.as_deref()
729 }
730
731 /// Substrate-canonical per-`Caixa` `:nome` universal-axis DNS-1123-
732 /// label caixa-identity scalar accessor every consumer of the top-
733 /// level manifest's identity axis keys off — returns the author-
734 /// declared `:nome` byte-string verbatim as an `&str`, borrowed from
735 /// the typed slot's own `String` storage. Non-optional (`:nome` is
736 /// a required-axis scalar every `defcaixa` form must supply; the
737 /// [`Self::from_lisp`] derive rejects an omitted / non-string
738 /// `:nome` at parse time, so a `Caixa` past parse definitionally
739 /// carries a non-`None` `:nome`).
740 ///
741 /// The `:nome` slot carries the universal-axis DNS-1123-label
742 /// caixa-identity every kind of caixa emits under (CAIXA-SDLC §I —
743 /// the primary identity axis every `defcaixa` form supplies
744 /// alongside `:versao` / `:kind`; the substrate-wide identity every
745 /// other typed surface that names a caixa reaches through — `:deps`
746 /// entries, `:membros` entries, `:children` entries, the
747 /// `lareira-<nome>` Helm chart name every per-Servico renderer
748 /// derives, the `pleme-program-<nome>` label every per-Aplicacao
749 /// renderer emits) — the typed slot's `String` accept-set (empty
750 /// rejected through [`ManifestError::NomeEmpty`], DNS-1123-shape-
751 /// invalid rejected through [`ManifestError::NomeInvalid`] past
752 /// the shared [`crate::render::require_valid_dns_1123_label`] gate
753 /// the peer name axes each land on, joint-length-with-`lareira-`-
754 /// prefix rejected through
755 /// [`ManifestError::NomeChartNameBudgetExceeded`] past
756 /// [`crate::render::is_lareira_chart_name_shape`]) maps onto every
757 /// load-bearing downstream consumer the substrate carries — the
758 /// two universal-axis validate gates at caixa-build time
759 /// ([`Self::validate_nome`] + [`Self::validate_nome_chart_name_budget`]),
760 /// [`crate::lareira_chart_name`]'s `lareira-<nome>` Helm chart-name
761 /// derivation every per-Servico renderer keys off, the caixa-helm
762 /// `Chart.yaml`'s `name:` axis, caixa-flux's `programs.yaml` entry
763 /// `name:` axis, caixa-mesh's Cilium `CiliumNetworkPolicy` /
764 /// `HTTPRoute` per-Aplicacao name axes at
765 /// caixa-mesh/src/lib.rs:{2650, 2797, 2919, 2925},
766 /// [`crate::pleme_program_selector`] /
767 /// [`crate::pleme_program_in_aplicacao_selector`] label-selector
768 /// derivations, and every future substrate renderer that emits an
769 /// artifact keyed by the caixa's identity.
770 ///
771 /// Prior to this lift the `.nome` field was accessed inline at a
772 /// dozen production sites across `caixa-core` (the two universal-
773 /// axis validate gates + [`Dep::validate`]-adjacent duplicate
774 /// tracking), `caixa-helm` (the `lareira_chart_name` fold, the
775 /// `ChartYaml.name` / `ChartYaml.description` / `Chart.yaml`
776 /// `keywords` fallback), `caixa-flux` (the `programs.yaml`
777 /// entry `name:` fold, the `flux_kustomization_source_subtree`
778 /// per-cluster subpath derivation), and `caixa-mesh` (the
779 /// `pleme_program_in_aplicacao_selector` label-selector fold, the
780 /// `cilium_network_policy_name` / `gateway_api_http_route_name`
781 /// per-CR name derivations, the `LABEL_APLICACAO` labels-map
782 /// insert) — a dozen open-coded field-accesses that expressed no
783 /// compile-time link back to the typed slot. A future extension of
784 /// the `:nome` axis to a richer author surface — a per-`:nome`
785 /// structured `CaixaIdentity` newtype that carries the joint-
786 /// length-with-prefix invariant [`Self::validate_nome_chart_name_budget`]
787 /// enforces at the type level (rather than as a validate-time
788 /// gate), a per-registry `:nome` namespacing overlay the M4 CR
789 /// materializer resolves per-CR (the "`pleme-io/checkout` vs
790 /// `partner-org/checkout` collision" arm the multi-tenant-registry
791 /// story acknowledges), a promotion of the plain `String` byte-
792 /// string to a richer `CaixaNome` newtype discriminated on
793 /// namespace prefix — would have had to be threaded through every
794 /// open-coded copy in lockstep or the two validate gates and the
795 /// dozen emit paths would silently disagree on which identity a
796 /// given [`Caixa`] resolves to (an author's `:nome "checkout"`
797 /// would satisfy validate while one of the emit paths silently
798 /// rendered a drifted other identity, or vice versa). Lifting the
799 /// resolution to a typed method on the substrate primitive means
800 /// every downstream consumer of the caixa's per-`Caixa` identity
801 /// surface reaches for exactly one typed dispatch — the resolver's
802 /// accept-set migrates as a unit on any future axis addition.
803 ///
804 /// First outer top-level [`Caixa`] `&str`-return required-scalar
805 /// accessor — opens the "outer [`Caixa`] `&str` required-scalar"
806 /// projection pattern the sibling per-`Caixa` `:versao` future lift
807 /// folds on. Sibling in shape to the peer per-`:membros`
808 /// [`crate::aplicacao::Membro::nome`] (4a32abf) / per-`:contratos`
809 /// [`crate::aplicacao::WitContract::source`] /
810 /// [`crate::aplicacao::WitContract::destination`] (7f0fd43),
811 /// [`crate::aplicacao::WitContract::world_ref`] (0804823),
812 /// [`crate::aplicacao::Membro::versao_requirement`] (a40b0e3),
813 /// [`crate::aplicacao::Entrada::destination`] (6db982c),
814 /// [`crate::aplicacao::CircuitBreaker::max_failures`] (3a74062),
815 /// per-sub-struct required-axis accessors carry on the sibling M3
816 /// mesh-slot-atom scalar-value axes, extended here to open the
817 /// outer top-level [`Caixa`] `&str`-return required-scalar surface.
818 /// Named `nome()` to match the storage field's name; the accessor's
819 /// identity maps onto the canonical CAIXA-SDLC §I vocabulary the
820 /// slot's docstring already carries.
821 #[must_use]
822 pub fn nome(&self) -> &str {
823 &self.nome
824 }
825
826 /// Substrate-canonical per-`Caixa` `:versao` universal-axis SemVer-2
827 /// pinned-version scalar accessor every consumer of the top-level
828 /// manifest's version axis keys off — returns the author-declared
829 /// `:versao` byte-string verbatim as an `&str`, borrowed from the
830 /// typed slot's own `String` storage. Non-optional (`:versao` is a
831 /// required-axis scalar every `defcaixa` form must supply alongside
832 /// `:nome` / `:kind`; the [`Self::from_lisp`] derive rejects an
833 /// omitted / non-string `:versao` at parse time, so a `Caixa` past
834 /// parse definitionally carries a non-`None` `:versao`).
835 ///
836 /// The `:versao` slot carries the universal-axis SemVer-2
837 /// concrete-version body every kind of caixa emits under
838 /// (CAIXA-SDLC §I — the required-scalar every `defcaixa` form
839 /// supplies alongside `:nome` / `:kind`; the substrate-wide
840 /// pinned-version every downstream artifact-emitting consumer
841 /// composes under — the `lareira-<nome>` Helm chart's `Chart.yaml`
842 /// `version:` + `appVersion:` axes, the `feira publish` Zig-style
843 /// `v<versao>` git tag the [`crate::DEFAULT_PUBLISH_TAG_PREFIX`]
844 /// prefix composes on top of, the programs.yaml entry's `versao:`
845 /// value the `lareira-fleet-programs` aggregator carries onto each
846 /// rendered `ComputeUnit`, the OCI image's `:v<versao>` / `:latest`
847 /// tags every substrate-side `skopeo push` writes, the lacre
848 /// closure's pinned `concrete_versao`, and the `:upgrade-from :from`
849 /// prior-version references peers in the exact same SemVer-2 shape).
850 /// The typed slot's `String` accept-set (empty rejected through
851 /// [`ManifestError::VersaoEmpty`], SemVer-2-shape-invalid rejected
852 /// through [`ManifestError::VersaoInvalid`] past
853 /// [`semver::Version::parse`]) maps onto every load-bearing
854 /// downstream consumer the substrate carries — the [`Self::validate_versao`]
855 /// universal-axis validate gate at caixa-build time, the
856 /// [`crate::CaixaVersion::parse`] typed-wrapper resolver,
857 /// [`caixa-helm`]'s `Chart.yaml` `version:` / `appVersion:` fold,
858 /// [`caixa-flux`]'s `programs.yaml` entry `versao:` fold + the
859 /// `cluster_bundle` `GitRepository` `ref: { tag: v<versao> }`
860 /// derivation, [`caixa-mesh`]'s per-Aplicacao `programs.yaml` fan-
861 /// out entry `versao:` fold, [`caixa-feira`]'s `feira publish` git-
862 /// tag derivation (`format!("{prefix}{versao}")`), and every future
863 /// substrate renderer that emits an artifact keyed by the caixa's
864 /// pinned version.
865 ///
866 /// Prior to this lift the `.versao` field was accessed inline at a
867 /// dozen production sites across `caixa-core` (the universal-axis
868 /// [`Self::validate_versao`] gate + [`Dep::validate`]-adjacent
869 /// version-shape gates), `caixa-helm` (the `ChartYaml.version` /
870 /// `ChartYaml.app_version` folds), `caixa-flux` (the `programs.yaml`
871 /// entry `versao:` fold, the `cluster_bundle` `GitRepository` `ref:
872 /// { tag: v<versao> }` derivation), `caixa-mesh` (the per-Aplicacao
873 /// `programs.yaml` fan-out entry `versao:` fold), and `caixa-feira`
874 /// (the `feira publish` git-tag derivation + the `feira app graph` /
875 /// `feira app deploy` diagnostic renderers) — a dozen open-coded
876 /// field-accesses that expressed no compile-time link back to the
877 /// typed slot. A future extension of the `:versao` axis to a richer
878 /// author surface — a per-`:versao` structured `CaixaVersion` at the
879 /// storage layer (the substrate already carries a `CaixaVersion`
880 /// newtype at [`crate::version::CaixaVersion`], deferred until the
881 /// serde-transparent-newtype-through-DeriveTataraDomain path lands),
882 /// a per-registry `:versao` immutability overlay the M4 CR
883 /// materializer enforces per-CR, a promotion of the plain `String`
884 /// byte-string to a richer `PinnedVersao` newtype discriminated on
885 /// SemVer-2 pre-release / build-metadata presence — would have had
886 /// to be threaded through every open-coded copy in lockstep or the
887 /// validate gate and the dozen emit paths would silently disagree
888 /// on which version a given [`Caixa`] resolves to (an author's
889 /// `:versao "0.1.0"` would satisfy validate while one of the emit
890 /// paths silently rendered a drifted other version, or vice versa).
891 /// Lifting the resolution to a typed method on the substrate
892 /// primitive means every downstream consumer of the caixa's
893 /// per-`Caixa` pinned-version surface reaches for exactly one typed
894 /// dispatch — the resolver's accept-set migrates as a unit on any
895 /// future axis addition.
896 ///
897 /// Second outer top-level [`Caixa`] `&str`-return required-scalar
898 /// accessor — folds on the "outer [`Caixa`] `&str` required-scalar"
899 /// projection pattern the sibling per-`Caixa` [`Self::nome`]
900 /// (e6b7d97) opened. Sibling in shape to the peer per-`:membros`
901 /// [`crate::aplicacao::Membro::versao_requirement`] (4127bb6) /
902 /// per-`:children` [`crate::supervisor::ChildSpec::versao_requirement`]
903 /// (2c053c8) / per-`:upgrade-from` [`crate::UpgradeFromEntry::prior_versao`]
904 /// (75d27a8) per-sub-struct `:versao`-shaped `&str`-return accessors
905 /// on the sibling per-typed-slot version-carrier axes, extended here
906 /// to close the second outer top-level [`Caixa`] required-`&str`-
907 /// carrying axis so the two universal-axis identity-carrying
908 /// scalars every `defcaixa` form supplies (`:nome` + `:versao`)
909 /// share the same "one typed dispatch per axis" discipline. Named
910 /// `versao()` to match the storage field's name; the accessor's
911 /// identity maps onto the canonical CAIXA-SDLC §I vocabulary the
912 /// slot's docstring already carries.
913 #[must_use]
914 pub fn versao(&self) -> &str {
915 &self.versao
916 }
917
918 /// Substrate-canonical per-`Caixa` `:kind` universal-axis
919 /// closed-set-enum discriminant accessor every consumer of the top-
920 /// level manifest's kind axis keys off — returns the author-declared
921 /// `:kind` variant verbatim as a [`CaixaKind`], `Copy`-projected
922 /// from the typed slot's own [`CaixaKind`] storage. Non-optional
923 /// (`:kind` is a required-axis discriminant every `defcaixa` form
924 /// must supply alongside `:nome` / `:versao`; the [`Self::from_lisp`]
925 /// derive rejects an omitted / non-symbol `:kind` at parse time, so
926 /// a `Caixa` past parse definitionally carries a valid [`CaixaKind`]
927 /// variant).
928 ///
929 /// The `:kind` slot carries the universal-axis closed-set typed-
930 /// discriminant every substrate-side dispatch keys off (CAIXA-SDLC
931 /// §I — the primary shape gate every renderer / verifier /
932 /// operator branches on; the five variants `Biblioteca` /
933 /// `Binario` / `Servico` / `Supervisor` / `Aplicacao` partition
934 /// the caixa surface into disjoint runtime contracts) — the typed
935 /// slot's [`CaixaKind`] accept-set (parse-time-rejected non-symbol
936 /// values through the derive-macro's symbol-arm gate, exhaustively
937 /// matched at every downstream dispatch site) maps onto every
938 /// load-bearing downstream consumer the substrate carries:
939 ///
940 /// - [`crate::render::require_kind`]'s per-renderer entry-gate
941 /// predicate — the canonical two-line
942 /// `require_kind(caixa, Servico)?` prelude every per-Servico
943 /// renderer (`caixa-helm`, `caixa-flux`, the future `caixa-otel`
944 /// / per-Servico OCI packager / M4 `wasm.pleme.io/v1alpha1/
945 /// ComputeUnit` CR materializer) runs at its entry-point,
946 /// alongside the [`crate::render::KindMismatch`] error carrier's
947 /// `actual:` field the diagnostic surfaces to name the offending
948 /// caixa's variant.
949 /// - [`Self::aplicacao_view`]'s + [`Self::supervisor_view`]'s
950 /// per-view kind-gate binding — the two `Option<TypedSpec>`
951 /// `_view` composers that fold the flat mesh-slot / supervisor-
952 /// slot columns into their typed sub-spec only when the kind
953 /// matches (returns `None` otherwise); the future per-Servico
954 /// M2-view composer (`servico_view`) will follow the same shape.
955 /// - [`Self::declared_foreign_code_slots`]'s per-slot kind-
956 /// coherence gate — the `!self.kind.requires_exe()` /
957 /// `!self.kind.requires_servicos()` predicates that fence
958 /// each code-surface slot from the wrong owning kind.
959 /// - [`crate::LayoutInvariants::verify`]'s kind ↔ code-surface
960 /// coherence gates — the six `caixa.kind == CaixaKind::X` /
961 /// `caixa.kind != CaixaKind::X` predicates and the four kind-
962 /// coherence error carriers (`SupervisorOwnsCode` /
963 /// `AplicacaoOwnsCode` / `MeshSlotsOnNonAplicacao` /
964 /// `SupervisorSlotsOnNonSupervisor` / `ServicoSlotsOnNonServico`
965 /// / `ForeignCodeSlot`) which each name the offending caixa's
966 /// variant in their `kind:` field.
967 ///
968 /// Prior to this lift the `.kind` field was accessed inline at
969 /// twenty-plus production sites across `caixa-core` (the
970 /// [`crate::render::require_kind`] entry-gate predicate + the
971 /// [`crate::render::KindMismatch`] `actual:` field, the two `_view`
972 /// composers, the `declared_foreign_code_slots` per-slot kind-
973 /// coherence gate, and the six [`crate::LayoutInvariants::verify`]
974 /// kind ↔ code-surface predicates + four error carriers) — a score
975 /// of open-coded field-accesses that expressed no compile-time link
976 /// back to the typed slot. A future extension of the `:kind` axis
977 /// to a richer author surface — a per-`:kind` sub-variant discriminant
978 /// (e.g. `Servico(ServicoRuntime)` splitting the current single
979 /// variant across the wasm-component / legacy-container / native-
980 /// binary runtime axes the M5 roadmap acknowledges), a per-cluster
981 /// kind-overlay the M4 CR materializer resolves per-CR (the
982 /// "cluster policy demotes `Aplicacao` to `Servico` on a single-
983 /// tenant cluster" arm), a promotion of the plain [`CaixaKind`]
984 /// enum to a richer `KindWithRuntime` discriminated on the
985 /// component-model world axis — would have had to be threaded
986 /// through every open-coded copy in lockstep or the entry gate,
987 /// the view composers, and the layout invariants would silently
988 /// disagree on which kind a given [`Caixa`] resolves to. Lifting
989 /// the resolution to a typed method on the substrate primitive
990 /// means every downstream consumer of the caixa's per-`Caixa`
991 /// kind surface reaches for exactly one typed dispatch — the
992 /// resolver's accept-set migrates as a unit on any future axis
993 /// addition.
994 ///
995 /// First outer top-level [`Caixa`] `Copy`-return required-enum-
996 /// discriminant accessor — opens the "outer [`Caixa`] `Copy`-return
997 /// required-discriminant" projection pattern. Sibling in shape to
998 /// the peer per-`:supervisor` [`crate::supervisor::SupervisorSpec::estrategia`]
999 /// (eafb619), per-`:placement` [`crate::aplicacao::Placement::estrategia`]
1000 /// (921fe1b), and per-`:children` [`crate::supervisor::ChildSpec::restart`]
1001 /// (dfb4a81) `Copy`-return closed-set-enum discriminant accessors
1002 /// on the sibling nested-spec typed-slot discriminator axes,
1003 /// extended here to the outer top-level [`Caixa`] universal-axis
1004 /// surface. Named `kind()` to match the storage field's name;
1005 /// the accessor's identity maps onto the canonical CAIXA-SDLC §I
1006 /// vocabulary the slot's docstring already carries.
1007 #[must_use]
1008 pub fn kind(&self) -> CaixaKind {
1009 self.kind
1010 }
1011
1012 /// Substrate-canonical per-`Caixa` `:autores` universal-axis
1013 /// maintainer-name-list slice-accessor every consumer of the top-
1014 /// level manifest's maintainer axis keys off — returns the author-
1015 /// declared `:autores` list verbatim as a `&[String]` slice-view over
1016 /// the same backing buffer the raw `self.autores.as_slice()` field
1017 /// access borrows from. Empty-list-carrying (`:autores` is a default-
1018 /// empty axis every `defcaixa` form supplies with an empty `()` when
1019 /// unset; the [`Self::from_lisp`] derive folds an omitted `:autores`
1020 /// through `#[serde(default)]` to `Vec::new()`, so a `Caixa` past
1021 /// parse definitionally carries a `Vec<String>` slot — possibly
1022 /// empty — and the returned `&[String]` degenerates to an empty
1023 /// slice on that arm without any silent `None` collapse).
1024 ///
1025 /// The `:autores` slot carries the universal-axis maintainer-name
1026 /// list every kind of caixa emits under (CAIXA-SDLC §I — the author-
1027 /// facing surface every `defcaixa` form supplies alongside `:nome` /
1028 /// `:versao` / `:kind`; the substrate-wide contact-carrying axis
1029 /// every downstream registry-facing artifact emits under) — the
1030 /// typed slot's `Vec<String>` accept-set (empty-per-entry rejected
1031 /// through [`ManifestError::AutorEmpty`], non-chart-maintainer-shape
1032 /// rejected through [`ManifestError::AutorInvalid`], cross-entry
1033 /// duplicate rejected through [`ManifestError::AutorDuplicate`]) maps
1034 /// onto every load-bearing downstream consumer the substrate carries
1035 /// — the [`Self::validate_autores`] universal-axis empty-per-entry +
1036 /// shape + duplicate gate at caixa-core/src/manifest.rs, the
1037 /// caixa-helm `build_chart_yaml` `maintainers:` fold at
1038 /// caixa-helm/src/lib.rs that walks each entry into a `Maintainer {
1039 /// name, email: None }` record, every future per-`Caixa` registry-
1040 /// facing renderer the CAIXA-SDLC §I roadmap acknowledges (the
1041 /// future `artifacthub.io/maintainers` `Chart.yaml` annotation the
1042 /// caixa-helm docstring alludes to at [`Self::validate_licenca`],
1043 /// the future per-cluster author-notification overlay the M4 CR
1044 /// materializer resolves per-CR).
1045 ///
1046 /// Prior to this lift the `.autores` field was accessed inline at
1047 /// two production sites — [`Self::validate_autores`]'s `for autor
1048 /// in &self.autores` walk that gates every entry through
1049 /// [`ManifestError::AutorEmpty`] / `AutorInvalid` / `AutorDuplicate`,
1050 /// and the caixa-helm `build_chart_yaml` `caixa.autores.iter().map(|a|
1051 /// Maintainer { name: a.clone(), email: None }).collect()` fold that
1052 /// materializes every entry into a `Chart.yaml` `maintainers:` row —
1053 /// two open-coded field-accesses that expressed no compile-time link
1054 /// back to the typed slot. A future extension of the `:autores` axis
1055 /// to a richer author surface — a per-`:autores` structured
1056 /// `Maintainer { name, email, url }` at the storage layer once the
1057 /// substrate absorbs `artifacthub.io/maintainers`' name+email+url
1058 /// tuple, a per-registry `:autores` allowlist the M4 CR materializer
1059 /// enforces per-CR (the "cluster policy demands every author declare
1060 /// an on-file `mailto:` contact" arm), a promotion of the plain
1061 /// `Vec<String>` byte-string list to a richer
1062 /// `Vec<ChartMaintainer>` newtype discriminated on the RFC-5322
1063 /// `<name> [<email>]` grammar the `is_chart_maintainer_name_shape`
1064 /// predicate already resolves through — would have had to be
1065 /// threaded through both open-coded copies in lockstep or the
1066 /// validate gate and the caixa-helm emit path would silently
1067 /// disagree on which authors a given [`Caixa`] resolves to (an
1068 /// author's `:autores ("alice" "bob")` would satisfy validate while
1069 /// the caixa-helm emit path silently rendered a drifted other
1070 /// maintainer list, or vice versa). Lifting the resolution to a
1071 /// typed method on the substrate primitive means every downstream
1072 /// consumer of the caixa's per-`Caixa` maintainer surface reaches
1073 /// for exactly one typed dispatch — the resolver's accept-set
1074 /// migrates as a unit on any future axis addition.
1075 ///
1076 /// First outer top-level [`Caixa`] `&[T]`-return slice-accessor —
1077 /// opens the "outer [`Caixa`] `&[T]` slice" projection pattern the
1078 /// sibling per-`Caixa` `:etiquetas` / `:deps` / `:deps-dev` / `:exe`
1079 /// / `:bibliotecas` / `:servicos` / `:upgrade-from` / `:children`
1080 /// future lifts fold on. Sibling in shape to the peer per-`:supervisor`
1081 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce), per-`:placement`
1082 /// [`crate::aplicacao::Placement::clusters`] (a6e18d7), per-`:membros`
1083 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36), per-`:contratos`
1084 /// [`crate::aplicacao::AplicacaoSpec::contratos`] (0dcc926), and
1085 /// per-`:upgrade-from :instructions` [`crate::upgrade::UpgradeFromEntry::instructions`]
1086 /// (0137e5a) `&[T]`-return slice accessors on the sibling per-M2 /
1087 /// per-M3 typed-slot list axes, extended here to the outer top-level
1088 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1089 /// `&Vec<String>`) because every downstream consumer of the author
1090 /// list treats it as a read-only sequence — the slice-view is the
1091 /// narrowest borrow that supports every present + roadmapped consumer
1092 /// (`.iter()`, `.len()`, `.is_empty()`) without leaking the backing
1093 /// `Vec`'s grow/push/reserve surface no consumer of the typed view
1094 /// reaches for (the storage-side `Vec` remains reachable through the
1095 /// `pub autores` field for the mutation-carrying serde round-trip and
1096 /// per-test fixture-mutation paths). Named `autores()` to match the
1097 /// storage field's name; the accessor's identity maps onto the
1098 /// canonical CAIXA-SDLC §I vocabulary the slot's docstring already
1099 /// carries.
1100 #[must_use]
1101 pub fn autores(&self) -> &[String] {
1102 self.autores.as_slice()
1103 }
1104
1105 /// Substrate-canonical per-`Caixa` `:etiquetas` universal-axis
1106 /// registry-search-tag-list slice-accessor every consumer of the
1107 /// top-level manifest's topical-tag axis keys off — returns the
1108 /// author-declared `:etiquetas` list verbatim as a `&[String]`
1109 /// slice-view over the same backing buffer the raw
1110 /// `self.etiquetas.as_slice()` field access borrows from. Empty-
1111 /// list-carrying (`:etiquetas` is a default-empty axis every
1112 /// `defcaixa` form supplies with an empty `()` when unset; the
1113 /// [`Self::from_lisp`] derive folds an omitted `:etiquetas` through
1114 /// `#[serde(default)]` to `Vec::new()`, so a `Caixa` past parse
1115 /// definitionally carries a `Vec<String>` slot — possibly empty —
1116 /// and the returned `&[String]` degenerates to an empty slice on
1117 /// that arm without any silent `None` collapse).
1118 ///
1119 /// The `:etiquetas` slot carries the universal-axis topical-tag
1120 /// list every kind of caixa emits under (CAIXA-SDLC §I — the
1121 /// author-facing surface every `defcaixa` form supplies alongside
1122 /// `:nome` / `:versao` / `:kind`; the substrate-wide registry-
1123 /// search-facing axis every downstream registry-facing artifact
1124 /// emits under) — the typed slot's `Vec<String>` accept-set
1125 /// (empty-per-entry rejected through [`ManifestError::EtiquetaEmpty`],
1126 /// non-chart-keyword-shape rejected through
1127 /// [`ManifestError::EtiquetaInvalid`], cross-entry duplicate
1128 /// rejected through [`ManifestError::EtiquetaDuplicate`]) maps onto
1129 /// every load-bearing downstream consumer the substrate carries —
1130 /// the [`Self::validate_etiquetas`] universal-axis empty-per-entry
1131 /// + shape + duplicate gate at caixa-core/src/manifest.rs, the
1132 /// caixa-helm `build_chart_yaml` `keywords:` fold at
1133 /// caixa-helm/src/lib.rs that walks each entry into the rendered
1134 /// `Chart.yaml` `keywords:` array (chained with the
1135 /// [`crate::LAREIRA_CHART_KEYWORDS`] substrate-wide floor set and
1136 /// dedup'd through a `BTreeSet` at emit time), every future per-
1137 /// `Caixa` registry-facing renderer the CAIXA-SDLC §I roadmap
1138 /// acknowledges (the future `artifacthub.io/keywords` `Chart.yaml`
1139 /// annotation, the future per-cluster tag-notification overlay the
1140 /// M4 CR materializer resolves per-CR).
1141 ///
1142 /// Prior to this lift the `.etiquetas` field was accessed inline at
1143 /// two production sites — [`Self::validate_etiquetas`]'s `for
1144 /// etiqueta in &self.etiquetas` walk that gates every entry through
1145 /// [`ManifestError::EtiquetaEmpty`] / `EtiquetaInvalid` /
1146 /// `EtiquetaDuplicate`, and the caixa-helm `build_chart_yaml`
1147 /// `caixa.etiquetas.iter().cloned().chain(...)` fold that
1148 /// materializes every entry into a `Chart.yaml` `keywords:` row —
1149 /// two open-coded field-accesses that expressed no compile-time
1150 /// link back to the typed slot. A future extension of the
1151 /// `:etiquetas` axis to a richer tag surface — a per-`:etiquetas`
1152 /// structured `ChartKeyword { name, uri, category }` at the storage
1153 /// layer once the substrate absorbs `artifacthub.io/keywords`
1154 /// richer tag tuple, a per-registry `:etiquetas` allowlist the M4
1155 /// CR materializer enforces per-CR (the "cluster policy demands
1156 /// every tag come from a substrate-approved taxonomy" arm), a
1157 /// promotion of the plain `Vec<String>` byte-string list to a
1158 /// richer `Vec<ChartKeyword>` newtype discriminated on the DNS-
1159 /// 1123-label-shaped grammar the `is_chart_keyword_shape` predicate
1160 /// already resolves through — would have had to be threaded through
1161 /// both open-coded copies in lockstep or the validate gate and the
1162 /// caixa-helm emit path would silently disagree on which tags a
1163 /// given [`Caixa`] resolves to (an author's `:etiquetas ("demo"
1164 /// "aplicacao")` would satisfy validate while the caixa-helm emit
1165 /// path silently rendered a drifted other keyword list, or vice
1166 /// versa). Lifting the resolution to a typed method on the
1167 /// substrate primitive means every downstream consumer of the
1168 /// caixa's per-`Caixa` topical-tag surface reaches for exactly one
1169 /// typed dispatch — the resolver's accept-set migrates as a unit
1170 /// on any future axis addition.
1171 ///
1172 /// Second outer top-level [`Caixa`] `&[T]`-return slice-accessor —
1173 /// folds on the "outer [`Caixa`] `&[T]` slice" projection pattern
1174 /// [`Self::autores`] (b5d813f) opened, sibling in shape and
1175 /// idiom. The remaining unlifted outer-`Caixa` slice-carrying axes
1176 /// (`:deps` / `:deps-dev` / `:exe` / `:bibliotecas` / `:servicos`
1177 /// / `:upgrade-from` / `:children` / `:membros` / `:contratos`)
1178 /// fold onto the same pattern in future lifts. Sibling in shape to
1179 /// the peer per-`:supervisor`
1180 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
1181 /// per-`:placement` [`crate::aplicacao::Placement::clusters`]
1182 /// (a6e18d7), per-`:membros`
1183 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
1184 /// per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
1185 /// (0dcc926), and per-`:upgrade-from :instructions`
1186 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1187 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1188 /// typed-slot list axes, extended here to the outer top-level
1189 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1190 /// `&Vec<String>`) because every downstream consumer of the tag
1191 /// list treats it as a read-only sequence — the slice-view is the
1192 /// narrowest borrow that supports every present + roadmapped
1193 /// consumer (`.iter()`, `.len()`, `.is_empty()`) without leaking
1194 /// the backing `Vec`'s grow/push/reserve surface no consumer of
1195 /// the typed view reaches for (the storage-side `Vec` remains
1196 /// reachable through the `pub etiquetas` field for the mutation-
1197 /// carrying serde round-trip and per-test fixture-mutation paths).
1198 /// Named `etiquetas()` to match the storage field's name; the
1199 /// accessor's identity maps onto the canonical CAIXA-SDLC §I
1200 /// vocabulary the slot's docstring already carries.
1201 #[must_use]
1202 pub fn etiquetas(&self) -> &[String] {
1203 self.etiquetas.as_slice()
1204 }
1205
1206 /// Substrate-canonical per-`Caixa` `:bibliotecas` universal-axis
1207 /// library-source-path-list slice-accessor every consumer of the
1208 /// top-level manifest's Biblioteca-source axis keys off — returns
1209 /// the author-declared `:bibliotecas` list verbatim as a
1210 /// `&[String]` slice-view over the same backing buffer the raw
1211 /// `self.bibliotecas.as_slice()` field access borrows from. Empty-
1212 /// list-carrying (`:bibliotecas` is a default-empty axis every
1213 /// `defcaixa` form supplies with an empty `()` when unset; the
1214 /// [`Self::from_lisp`] derive folds an omitted `:bibliotecas`
1215 /// through `#[serde(default)]` to `Vec::new()`, so a `Caixa` past
1216 /// parse definitionally carries a `Vec<String>` slot — possibly
1217 /// empty — and the returned `&[String]` degenerates to an empty
1218 /// slice on that arm without any silent `None` collapse).
1219 ///
1220 /// The `:bibliotecas` slot carries the universal-axis lisp-library
1221 /// entry-path list every `:kind Biblioteca` caixa emits under
1222 /// (CAIXA-SDLC §I — the author-facing surface every `defcaixa`
1223 /// form supplies alongside `:nome` / `:versao` / `:kind`; the
1224 /// substrate-wide library-carrier axis every downstream
1225 /// authoring-facing consumer keys off) — the typed slot's
1226 /// `Vec<String>` accept-set (empty-per-entry rejected through
1227 /// [`ManifestError::CodePathEmpty { slot: ":bibliotecas" }`],
1228 /// non-sandboxed-relative-shape rejected through
1229 /// [`ManifestError::CodePathShape`], non-`.lisp`-extension rejected
1230 /// through [`ManifestError::CodePathNonLispExtension`], cross-entry
1231 /// duplicate rejected through [`ManifestError::CodePathDuplicate`])
1232 /// maps onto every load-bearing downstream consumer the substrate
1233 /// carries — the [`crate::LayoutInvariants`] Biblioteca-arm
1234 /// empty-check + per-entry file-exists loop at
1235 /// caixa-core/src/layout.rs that gates each entry through
1236 /// [`crate::LayoutError::MissingLib`] / `MissingEntry`, the
1237 /// [`Self::validate_code_paths`] per-slot shape gate at
1238 /// caixa-core/src/manifest.rs that walks each entry through the
1239 /// sandbox-relative / `.lisp`-extension / cross-entry duplicate
1240 /// gates, the `feira build` per-entry `tatara_lisp::read` parse
1241 /// walk at caixa-feira/src/cmd/build.rs that phase-1-checks each
1242 /// declared library file for lexical / structural errors before
1243 /// downstream `importar` resolution, every future per-`Caixa`
1244 /// library-facing renderer the CAIXA-SDLC §I roadmap acknowledges
1245 /// (the future `tatara-lispc` compilation entry the docstring at
1246 /// caixa-feira/src/cmd/build.rs alludes to, the future per-cluster
1247 /// bytecode-caching overlay the M4 CR materializer resolves per-CR,
1248 /// the future `caixa-lsp` per-library semantic-token stream the
1249 /// caixa-lsp docstring roadmaps).
1250 ///
1251 /// Prior to this lift the `.bibliotecas` field was accessed inline
1252 /// at three production sites — [`crate::LayoutInvariants`]'s
1253 /// `caixa.bibliotecas.is_empty()` `MissingLib`-arm gate + `for p
1254 /// in &caixa.bibliotecas` `MissingEntry` walk that gates each
1255 /// declared library path through the on-disk-existence check,
1256 /// the compound-code-path `has_code = !caixa.bibliotecas.is_empty()
1257 /// || !caixa.exe.is_empty() || !caixa.servicos.is_empty()` OR-fold
1258 /// on the [`crate::LayoutError::SupervisorOwnsCode`] /
1259 /// `AplicacaoOwnsCode` kind-coherence gate, and the `feira build`
1260 /// per-entry `for entry in &caixa.bibliotecas` + `caixa.bibliotecas.
1261 /// len()` phase-1 tatara-lispc-precursor parse walk — three open-
1262 /// coded field-accesses that expressed no compile-time link back
1263 /// to the typed slot. A future extension of the `:bibliotecas`
1264 /// axis to a richer library surface — a per-`:bibliotecas`
1265 /// structured `BibliotecaEntry { path, edition, exports }` at the
1266 /// storage layer once the substrate absorbs the per-library
1267 /// language-edition + explicit-exports tuple the tatara-lisp
1268 /// module-system roadmap acknowledges, a per-registry
1269 /// `:bibliotecas` allowlist the M4 CR materializer enforces
1270 /// per-CR (the "cluster policy demands every biblioteca declare
1271 /// its own :edicao" arm), a promotion of the plain `Vec<String>`
1272 /// byte-string list to a richer `Vec<LibraryPath>` newtype
1273 /// discriminated on the `lib/<nome>.lisp`-shape grammar the
1274 /// [`crate::render::is_sandboxed_relative_path`] +
1275 /// [`crate::render::is_lisp_extension`] predicates already resolve
1276 /// through — would have had to be threaded through all three
1277 /// open-coded copies in lockstep or the layout gate, the shape
1278 /// validator, and the `feira build` phase-1 parse walk would
1279 /// silently disagree on which library paths a given [`Caixa`]
1280 /// resolves to (an author's `:bibliotecas ("lib/foo.lisp"
1281 /// "lib/bar.lisp")` would satisfy layout while `feira build`
1282 /// silently parsed a drifted other list, or vice versa). Lifting
1283 /// the resolution to a typed method on the substrate primitive
1284 /// means every downstream consumer of the caixa's per-`Caixa`
1285 /// library-source surface reaches for exactly one typed dispatch
1286 /// — the resolver's accept-set migrates as a unit on any future
1287 /// axis addition.
1288 ///
1289 /// Third outer top-level [`Caixa`] `&[T]`-return slice-accessor —
1290 /// folds on the "outer [`Caixa`] `&[T]` slice" projection pattern
1291 /// [`Self::autores`] (b5d813f) opened and [`Self::etiquetas`]
1292 /// (78c7d3c) folded on, sibling in shape and idiom. The remaining
1293 /// unlifted outer-`Caixa` slice-carrying axes (`:deps` /
1294 /// `:deps-dev` / `:exe` / `:servicos` / `:upgrade-from` /
1295 /// `:children` / `:membros` / `:contratos`) fold onto the same
1296 /// pattern in future lifts. Sibling in shape to the peer
1297 /// per-`:supervisor`
1298 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
1299 /// per-`:placement` [`crate::aplicacao::Placement::clusters`]
1300 /// (a6e18d7), per-`:membros`
1301 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
1302 /// per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
1303 /// (0dcc926), and per-`:upgrade-from :instructions`
1304 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1305 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1306 /// typed-slot list axes, extended here to the outer top-level
1307 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1308 /// `&Vec<String>`) because every downstream consumer of the
1309 /// library-source list treats it as a read-only sequence — the
1310 /// slice-view is the narrowest borrow that supports every
1311 /// present + roadmapped consumer (`.iter()`, `.len()`,
1312 /// `.is_empty()`) without leaking the backing `Vec`'s
1313 /// grow/push/reserve surface no consumer of the typed view
1314 /// reaches for (the storage-side `Vec` remains reachable through
1315 /// the `pub bibliotecas` field for the mutation-carrying serde
1316 /// round-trip and per-test fixture-mutation paths). Named
1317 /// `bibliotecas()` to match the storage field's name; the
1318 /// accessor's identity maps onto the canonical CAIXA-SDLC §I
1319 /// vocabulary the slot's docstring already carries.
1320 #[must_use]
1321 pub fn bibliotecas(&self) -> &[String] {
1322 self.bibliotecas.as_slice()
1323 }
1324
1325 /// Substrate-canonical per-`Caixa` `:exe` universal-axis
1326 /// nix-built-executable-entry-path-list slice-accessor every consumer
1327 /// of the top-level manifest's Binario-executable axis keys off —
1328 /// returns the author-declared `:exe` list verbatim as a `&[String]`
1329 /// slice-view over the same backing buffer the raw
1330 /// `self.exe.as_slice()` field access borrows from. Empty-list-
1331 /// carrying (`:exe` is a default-empty axis every `defcaixa` form
1332 /// supplies with an empty `()` when unset; the [`Self::from_lisp`]
1333 /// derive folds an omitted `:exe` through `#[serde(default)]` to
1334 /// `Vec::new()`, so a `Caixa` past parse definitionally carries a
1335 /// `Vec<String>` slot — possibly empty — and the returned `&[String]`
1336 /// degenerates to an empty slice on that arm without any silent
1337 /// `None` collapse).
1338 ///
1339 /// The `:exe` slot carries the universal-axis nix-built executable
1340 /// entry-path list every `:kind Binario` caixa emits under
1341 /// (CAIXA-SDLC §I — the author-facing surface every `defcaixa`
1342 /// form supplies alongside `:nome` / `:versao` / `:kind`; the
1343 /// substrate-wide `exe/`-directory-fenced entry-carrier axis every
1344 /// downstream flake-build-facing consumer keys off) — the typed
1345 /// slot's `Vec<String>` accept-set (empty-per-entry rejected
1346 /// through [`ManifestError::CodePathEmpty { slot: ":exe" }`],
1347 /// non-sandboxed-relative-shape rejected through
1348 /// [`ManifestError::CodePathShape`], cross-entry duplicate rejected
1349 /// through [`ManifestError::CodePathDuplicate`], out-of-`exe/`-
1350 /// directory paths rejected past the layout's
1351 /// [`crate::LayoutError::ExeOutsideDir`] `starts_with` fence) maps
1352 /// onto every load-bearing downstream consumer the substrate carries
1353 /// — the [`crate::LayoutInvariants`] Binario-arm empty-check +
1354 /// per-entry file-exists + `exe/`-directory-fence loop at
1355 /// caixa-core/src/layout.rs that gates each entry through
1356 /// [`crate::LayoutError::BinarioWithoutExe`] / `MissingEntry` /
1357 /// `ExeOutsideDir`, the compound `has_code` OR-fold on the
1358 /// [`crate::LayoutError::SupervisorOwnsCode`] /
1359 /// [`crate::LayoutError::AplicacaoOwnsCode`] kind-coherence gate
1360 /// that fences code-surface slots off from the two no-code kinds,
1361 /// [`Self::declared_foreign_code_slots`]'s `!self.exe.is_empty()`
1362 /// arm on the [`crate::LayoutError::ForeignCodeSlot`] gate that
1363 /// fences the `:exe` code surface off from every non-Binario code-
1364 /// running kind, [`Self::validate_code_paths`]'s per-slot shape gate
1365 /// that walks each entry through the sandbox-relative / cross-entry
1366 /// duplicate gates, every future per-`Caixa` executable-facing
1367 /// renderer the CAIXA-SDLC §I roadmap acknowledges (the future
1368 /// `caixa-flake` per-Binario `packages.<system>.<nome>` derivation
1369 /// entry the caixa-flake docstring roadmaps, the future per-cluster
1370 /// `nix-store` overlay the M4 CR materializer resolves per-CR, the
1371 /// future `feira nix` per-executable Binario-target emit path).
1372 ///
1373 /// Prior to this lift the `.exe` field was accessed inline at three
1374 /// production sites — the compound-code-path `has_code =
1375 /// !caixa.bibliotecas().is_empty() || !caixa.exe.is_empty() ||
1376 /// !caixa.servicos.is_empty()` OR-fold on the
1377 /// [`crate::LayoutError::SupervisorOwnsCode`] /
1378 /// `AplicacaoOwnsCode` kind-coherence gate, the Binario-arm
1379 /// `caixa.exe.is_empty()` [`crate::LayoutError::BinarioWithoutExe`]
1380 /// gate, the per-entry `for p in &caixa.exe`
1381 /// `MissingEntry`/`ExeOutsideDir` walk, and the
1382 /// [`Self::declared_foreign_code_slots`]'s
1383 /// `!self.exe.is_empty()` arm on the `ForeignCodeSlot` gate — four
1384 /// open-coded field-accesses that expressed no compile-time link
1385 /// back to the typed slot. A future extension of the `:exe` axis
1386 /// to a richer executable surface — a per-`:exe` structured
1387 /// `BinarioEntry { path, wrapper, capabilities }` at the storage
1388 /// layer once the substrate absorbs the per-executable
1389 /// nix-wrapper + linux-capabilities tuple the CAIXA-SDLC §I
1390 /// executable roadmap acknowledges, a per-registry `:exe` allowlist
1391 /// the M4 CR materializer enforces per-CR (the "cluster policy
1392 /// demands every Binario declare an explicit `:wrapper`" arm), a
1393 /// promotion of the plain `Vec<String>` byte-string list to a
1394 /// richer `Vec<ExecutablePath>` newtype discriminated on the
1395 /// `exe/<nome>`-shape grammar the layout's `starts_with(exe_dir)`
1396 /// fence already resolves through — would have had to be threaded
1397 /// through all four open-coded copies in lockstep or the layout
1398 /// gate, the shape validator, and the `feira nix` emit path would
1399 /// silently disagree on which executable paths a given [`Caixa`]
1400 /// resolves to (an author's `:exe ("exe/cli" "exe/serve")` would
1401 /// satisfy layout while `feira nix` silently packaged a drifted
1402 /// other list, or vice versa). Lifting the resolution to a typed
1403 /// method on the substrate primitive means every downstream
1404 /// consumer of the caixa's per-`Caixa` executable-source surface
1405 /// reaches for exactly one typed dispatch — the resolver's accept-
1406 /// set migrates as a unit on any future axis addition.
1407 ///
1408 /// Fourth outer top-level [`Caixa`] `&[T]`-return slice-accessor —
1409 /// folds on the "outer [`Caixa`] `&[T]` slice" projection pattern
1410 /// [`Self::autores`] (b5d813f) opened, [`Self::etiquetas`]
1411 /// (78c7d3c) folded on, and [`Self::bibliotecas`] (8a36c23) closed
1412 /// the universal-axis text-tag family of. Opens the outer-`Caixa`
1413 /// foreign-code-slot `&[T]` sub-family the sibling `:servicos`
1414 /// future lift closes onto (per the trio of code-surface list slots
1415 /// the [`Self::validate_code_paths`] per-slot dispatch tuple
1416 /// already carries — `:bibliotecas` + `:exe` + `:servicos`, of which
1417 /// `:bibliotecas` landed at 8a36c23 and `:servicos` remains as the
1418 /// last unlifted code-surface slot). Sibling in shape to the peer
1419 /// per-`:supervisor` [`crate::supervisor::SupervisorSpec::children`]
1420 /// (bc92bce), per-`:placement`
1421 /// [`crate::aplicacao::Placement::clusters`] (a6e18d7),
1422 /// per-`:membros` [`crate::aplicacao::AplicacaoSpec::membros`]
1423 /// (6c77e36), per-`:contratos`
1424 /// [`crate::aplicacao::AplicacaoSpec::contratos`] (0dcc926), and
1425 /// per-`:upgrade-from :instructions`
1426 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1427 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1428 /// typed-slot list axes, extended here to the outer top-level
1429 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1430 /// `&Vec<String>`) because every downstream consumer of the
1431 /// executable-source list treats it as a read-only sequence — the
1432 /// slice-view is the narrowest borrow that supports every
1433 /// present + roadmapped consumer (`.iter()`, `.len()`,
1434 /// `.is_empty()`) without leaking the backing `Vec`'s
1435 /// grow/push/reserve surface no consumer of the typed view
1436 /// reaches for (the storage-side `Vec` remains reachable through
1437 /// the `pub exe` field for the mutation-carrying serde
1438 /// round-trip and per-test fixture-mutation paths). Named `exe()`
1439 /// to match the storage field's name; the accessor's identity
1440 /// maps onto the canonical CAIXA-SDLC §I vocabulary the slot's
1441 /// docstring already carries.
1442 #[must_use]
1443 pub fn exe(&self) -> &[String] {
1444 self.exe.as_slice()
1445 }
1446
1447 /// Substrate-canonical per-`Caixa` `:servicos` universal-axis
1448 /// ComputeUnit-CR-YAML-entry-path-list slice-accessor every consumer
1449 /// of the top-level manifest's Servico-component axis keys off —
1450 /// returns the author-declared `:servicos` list verbatim as a
1451 /// `&[String]` slice-view over the same backing buffer the raw
1452 /// `self.servicos.as_slice()` field access borrows from. Empty-list-
1453 /// carrying (`:servicos` is a default-empty axis every `defcaixa`
1454 /// form supplies with an empty `()` when unset; the
1455 /// [`Self::from_lisp`] derive folds an omitted `:servicos` through
1456 /// `#[serde(default)]` to `Vec::new()`, so a `Caixa` past parse
1457 /// definitionally carries a `Vec<String>` slot — possibly empty —
1458 /// and the returned `&[String]` degenerates to an empty slice on
1459 /// that arm without any silent `None` collapse).
1460 ///
1461 /// The `:servicos` slot carries the universal-axis
1462 /// `.computeunit.yaml` ComputeUnit-CR entry-path list every
1463 /// `:kind Servico` caixa emits under (CAIXA-SDLC §I — the
1464 /// author-facing surface every `defcaixa` form supplies alongside
1465 /// `:nome` / `:versao` / `:kind`; the substrate-wide
1466 /// `servicos/`-directory-fenced entry-carrier axis every downstream
1467 /// Servico-facing renderer keys off) — the typed slot's
1468 /// `Vec<String>` accept-set (empty-per-entry rejected through
1469 /// [`ManifestError::CodePathEmpty { slot: ":servicos" }`],
1470 /// non-sandboxed-relative-shape rejected through
1471 /// [`ManifestError::CodePathShape`], non-`.computeunit.yaml`
1472 /// extension rejected through
1473 /// [`ManifestError::CodePathNonComputeUnitYamlExtension`], cross-
1474 /// entry duplicate rejected through
1475 /// [`ManifestError::CodePathDuplicate`], `len != 1` rejected by the
1476 /// V0 [`crate::ServicoCountMismatch`] gate on the per-Servico
1477 /// renderer entry-points, out-of-`servicos/`-directory paths
1478 /// rejected past the layout's [`crate::LayoutError::ServicoOutsideDir`]
1479 /// `starts_with` fence) maps onto every load-bearing downstream
1480 /// consumer the substrate carries — the [`crate::LayoutInvariants`]
1481 /// Servico-arm empty-check + per-entry file-exists + `servicos/`-
1482 /// directory-fence loop at caixa-core/src/layout.rs that gates each
1483 /// entry through [`crate::LayoutError::ServicoWithoutServicos`] /
1484 /// `MissingEntry` / `ServicoOutsideDir`, the compound `has_code`
1485 /// OR-fold on the [`crate::LayoutError::SupervisorOwnsCode`] /
1486 /// [`crate::LayoutError::AplicacaoOwnsCode`] kind-coherence gate
1487 /// that fences code-surface slots off from the two no-code kinds,
1488 /// [`Self::declared_foreign_code_slots`]'s
1489 /// `!self.servicos.is_empty()` arm on the
1490 /// [`crate::LayoutError::ForeignCodeSlot`] gate that fences the
1491 /// `:servicos` code surface off from every non-Servico code-running
1492 /// kind, [`Self::validate_code_paths`]'s per-slot shape gate that
1493 /// walks each entry through the sandbox-relative / `.computeunit.
1494 /// yaml`-extension / cross-entry duplicate gates, the
1495 /// [`crate::require_single_servico`] V0 singularity gate every
1496 /// per-Servico renderer entry-point runs through
1497 /// [`crate::require_v0_servico_shape`], the `feira chart` /
1498 /// `feira deploy` per-verb `first_servico_path` walk at
1499 /// caixa-feira/src/cmd/chart.rs that resolves the singleton
1500 /// ComputeUnit-CR file, every future per-`Caixa` Servico-facing
1501 /// renderer the CAIXA-SDLC §I roadmap acknowledges (the future
1502 /// per-Servico OCI packager, the future M4
1503 /// `wasm.pleme.io/v1alpha1/ComputeUnit` CR materializer, the future
1504 /// per-Servico OTel collector-config emit).
1505 ///
1506 /// Prior to this lift the `.servicos` field was accessed inline at
1507 /// five production sites — the compound-code-path `has_code =
1508 /// !caixa.bibliotecas().is_empty() || !caixa.exe().is_empty() ||
1509 /// !caixa.servicos.is_empty()` OR-fold on the
1510 /// [`crate::LayoutError::SupervisorOwnsCode`] /
1511 /// `AplicacaoOwnsCode` kind-coherence gate, the Servico-arm
1512 /// `caixa.servicos.is_empty()`
1513 /// [`crate::LayoutError::ServicoWithoutServicos`] gate, the
1514 /// per-entry `for p in &caixa.servicos`
1515 /// `MissingEntry`/`ServicoOutsideDir` walk, the
1516 /// [`Self::declared_foreign_code_slots`]'s
1517 /// `!self.servicos.is_empty()` arm on the `ForeignCodeSlot` gate,
1518 /// and the [`crate::require_single_servico`] V0 count gate's
1519 /// `caixa.servicos.len() == 1` / `caixa.servicos.len()` count
1520 /// projection (both the accept-arm predicate and the
1521 /// diagnostic-carrying `ServicoCountMismatch { count }`
1522 /// projection) — five open-coded field-accesses across three
1523 /// crates that expressed no compile-time link back to the typed
1524 /// slot. A future extension of the `:servicos` axis to a richer
1525 /// component surface — a per-`:servicos` structured
1526 /// `ServicoEntry { path, world, capabilities }` at the storage
1527 /// layer once the substrate absorbs the per-component WIT-world +
1528 /// capability-set tuple the CAIXA-SDLC §I Servico roadmap
1529 /// acknowledges, a per-registry `:servicos` allowlist the M4 CR
1530 /// materializer enforces per-CR (the "cluster policy demands every
1531 /// Servico declare an explicit `:world`" arm), a promotion of the
1532 /// plain `Vec<String>` byte-string list to a richer
1533 /// `Vec<ComputeUnitPath>` newtype discriminated on the
1534 /// `servicos/<nome>.computeunit.yaml`-shape grammar the layout's
1535 /// `starts_with(servicos_dir)` fence and the
1536 /// [`crate::render::is_computeunit_yaml_extension`] predicate
1537 /// already resolve through, a promotion of the V0 singleton
1538 /// contract to a multi-component `Vec<ComputeUnitPath>` past the M5
1539 /// component-model multi-world boundary — would have had to be
1540 /// threaded through all five open-coded copies in lockstep or the
1541 /// layout gate, the shape validator, the V0 count gate, and the
1542 /// `feira chart` / `feira deploy` entry-point walks would silently
1543 /// disagree on which ComputeUnit-CR paths a given [`Caixa`]
1544 /// resolves to (an author's `:servicos ("servicos/foo.computeunit.
1545 /// yaml")` would satisfy layout while `feira chart` silently
1546 /// packaged a drifted other list, or vice versa). Lifting the
1547 /// resolution to a typed method on the substrate primitive means
1548 /// every downstream consumer of the caixa's per-`Caixa`
1549 /// ComputeUnit-CR-source surface reaches for exactly one typed
1550 /// dispatch — the resolver's accept-set migrates as a unit on any
1551 /// future axis addition.
1552 ///
1553 /// Fifth and final outer top-level [`Caixa`] `&[T]`-return slice-
1554 /// accessor — folds on the "outer [`Caixa`] `&[T]` slice"
1555 /// projection pattern [`Self::autores`] (b5d813f) opened,
1556 /// [`Self::etiquetas`] (78c7d3c) folded on, [`Self::bibliotecas`]
1557 /// (8a36c23) closed the universal-axis text-tag family of, and
1558 /// [`Self::exe`] (65d9527) opened the foreign-code-slot sub-family
1559 /// of. Closes the outer-`Caixa` foreign-code-slot `&[T]` sub-family
1560 /// — with `:bibliotecas`, `:exe`, and `:servicos` now each carrying
1561 /// a substrate-canonical slice accessor, the trio of code-surface
1562 /// list slots the [`Self::validate_code_paths`] per-slot dispatch
1563 /// tuple carries is complete on the typed dispatch surface (the
1564 /// internal `[(":bibliotecas", &self.bibliotecas, ..), (":exe",
1565 /// &self.exe, ..), (":servicos", &self.servicos, ..)]` per-slot
1566 /// dispatch tuple's homogeneous `&Vec<String>`-typed shape blocks a
1567 /// per-element accessor swap in isolation — a future companion lift
1568 /// promotes the tuple's element type to `&[String]` and threads the
1569 /// triple of typed dispatches through as a unit). Sibling in shape
1570 /// to the peer per-`:supervisor`
1571 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
1572 /// per-`:placement` [`crate::aplicacao::Placement::clusters`]
1573 /// (a6e18d7), per-`:membros`
1574 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
1575 /// per-`:contratos`
1576 /// [`crate::aplicacao::AplicacaoSpec::contratos`] (0dcc926), and
1577 /// per-`:upgrade-from :instructions`
1578 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1579 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1580 /// typed-slot list axes, extended here to the outer top-level
1581 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1582 /// `&Vec<String>`) because every downstream consumer of the
1583 /// ComputeUnit-CR-source list treats it as a read-only sequence —
1584 /// the slice-view is the narrowest borrow that supports every
1585 /// present + roadmapped consumer (`.iter()`, `.len()`,
1586 /// `.is_empty()`, `.first()`) without leaking the backing `Vec`'s
1587 /// grow/push/reserve surface no consumer of the typed view reaches
1588 /// for (the storage-side `Vec` remains reachable through the
1589 /// `pub servicos` field for the mutation-carrying serde round-trip
1590 /// and per-test fixture-mutation paths, and for the
1591 /// [`Self::validate_code_paths`] per-slot dispatch tuple whose
1592 /// homogeneous-element-type shape carries the raw field access
1593 /// until the trio-closure lift promotes the tuple as a unit).
1594 /// Named `servicos()` to match the storage field's name; the
1595 /// accessor's identity maps onto the canonical CAIXA-SDLC §I
1596 /// vocabulary the slot's docstring already carries.
1597 #[must_use]
1598 pub fn servicos(&self) -> &[String] {
1599 self.servicos.as_slice()
1600 }
1601
1602 /// Substrate-canonical per-`Caixa` `:deps` universal-axis
1603 /// runtime-dependency-declaration-list slice-accessor every consumer
1604 /// of the top-level manifest's runtime-dep-graph axis keys off —
1605 /// returns the author-declared `:deps` list verbatim as a `&[Dep]`
1606 /// slice-view over the same backing buffer the raw
1607 /// `self.deps.as_slice()` field access borrows from. Empty-list-
1608 /// carrying (`:deps` is a default-empty axis every `defcaixa` form
1609 /// supplies with an empty `()` when unset; the [`Self::from_lisp`]
1610 /// derive folds an omitted `:deps` through `#[serde(default)]` to
1611 /// `Vec::new()`, so a `Caixa` past parse definitionally carries a
1612 /// `Vec<Dep>` slot — possibly empty — and the returned `&[Dep]`
1613 /// degenerates to an empty slice on that arm without any silent
1614 /// `None` collapse).
1615 ///
1616 /// The `:deps` slot carries the universal-axis runtime dependency
1617 /// list every kind of caixa emits under (CAIXA-SDLC §I — the author-
1618 /// facing surface every `defcaixa` form supplies alongside `:nome` /
1619 /// `:versao` / `:kind`; the substrate-wide runtime-closure-input axis
1620 /// every downstream resolver-facing artifact emits under) — the
1621 /// typed slot's `Vec<Dep>` accept-set (empty-`:nome` rejected through
1622 /// [`DepError::NomeEmpty`], non-DNS-1123-label `:nome` rejected
1623 /// through [`DepError::NomeInvalid`], malformed `:versao` rejected
1624 /// through [`DepError::VersaoInvalid`], empty `:fonte.repo` rejected
1625 /// through [`DepError::FonteRepoEmpty`], within-list duplicate `:nome`
1626 /// rejected through [`DepError::DuplicateNome { list: ":deps" }`])
1627 /// maps onto every load-bearing downstream consumer the substrate
1628 /// carries — the [`Self::validate_deps`] per-entry
1629 /// [`Dep::validate`] + within-list dedup walk at
1630 /// caixa-core/src/manifest.rs, the [`crate::dep::validate_no_self_dep`]
1631 /// cross-list self-reference gate at caixa-core/src/layout.rs that
1632 /// checks each entry against the caixa's own `:nome`, the
1633 /// caixa-resolver `for dep in &root.deps` closure walk at
1634 /// caixa-resolver/src/resolve.rs that seeds every git-clone target
1635 /// through the resolver's [`crate::Dep`]-keyed pipeline, the
1636 /// caixa-crd `caixa.deps.iter().map(dep_into_ref).collect()` fold at
1637 /// caixa-crd/src/conversion.rs that materializes each entry into the
1638 /// K8s `Caixa` CR's `spec.deps` field, every future per-`Caixa`
1639 /// resolver-facing renderer the CAIXA-SDLC §I roadmap acknowledges
1640 /// (the future per-cluster runtime-closure-audit overlay the M4 CR
1641 /// materializer resolves per-CR, the future `lacre.lisp` BLAKE3-
1642 /// closure emit walk the caixa-resolver docstring roadmaps).
1643 ///
1644 /// First outer top-level [`Caixa`] `&[Dep]`-return slice-accessor —
1645 /// opens the outer-`Caixa` dependency-slot `&[Dep]` sub-family the
1646 /// sibling `:deps-dev` future lift closes on. Peer of the closed
1647 /// outer-`Caixa` foreign-code-slot `&[String]` sub-family
1648 /// ([`Self::bibliotecas`] 8a36c23, [`Self::exe`] 65d9527,
1649 /// [`Self::servicos`] 611f78b) and the outer-`Caixa` universal-axis
1650 /// text-tag family ([`Self::autores`] b5d813f, [`Self::etiquetas`]
1651 /// 78c7d3c) — extends the "outer [`Caixa`] `&[T]` slice" projection
1652 /// pattern onto a novel element-type axis (`Dep` composite vs the
1653 /// prior sibling family's `String` scalar). Sibling in shape to the
1654 /// peer per-`:supervisor`
1655 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
1656 /// per-`:placement` [`crate::aplicacao::Placement::clusters`]
1657 /// (a6e18d7), per-`:membros`
1658 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
1659 /// per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
1660 /// (0dcc926), and per-`:upgrade-from :instructions`
1661 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1662 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1663 /// typed-slot list axes, extended here to the outer top-level
1664 /// [`Caixa`] universal-axis dep-graph surface. Returns `&[Dep]`
1665 /// (not `&Vec<Dep>`) because every downstream consumer of the
1666 /// runtime-dep list treats it as a read-only sequence — the slice-
1667 /// view is the narrowest borrow that supports every present +
1668 /// roadmapped consumer (`.iter()`, `.len()`, `.is_empty()`) without
1669 /// leaking the backing `Vec`'s grow/push/reserve surface no consumer
1670 /// of the typed view reaches for (the storage-side `Vec` remains
1671 /// reachable through the `pub deps` field for the mutation-carrying
1672 /// serde round-trip and per-test fixture-mutation paths). Named
1673 /// `deps()` to match the storage field's name; the accessor's
1674 /// identity maps onto the canonical CAIXA-SDLC §I vocabulary the
1675 /// slot's docstring already carries.
1676 #[must_use]
1677 pub fn deps(&self) -> &[Dep] {
1678 self.deps.as_slice()
1679 }
1680
1681 /// Substrate-canonical per-`Caixa` `:deps-dev` universal-axis
1682 /// development-only-dependency-declaration-list slice-accessor every
1683 /// consumer of the top-level manifest's dev-dep-graph axis keys off —
1684 /// returns the author-declared `:deps-dev` list verbatim as a `&[Dep]`
1685 /// slice-view over the same backing buffer the raw
1686 /// `self.deps_dev.as_slice()` field access borrows from. Empty-list-
1687 /// carrying (`:deps-dev` is a default-empty axis every `defcaixa`
1688 /// form supplies with an empty `()` when unset; the
1689 /// [`Self::from_lisp`] derive folds an omitted `:deps-dev` through
1690 /// `#[serde(default)]` to `Vec::new()`, so a `Caixa` past parse
1691 /// definitionally carries a `Vec<Dep>` slot — possibly empty — and
1692 /// the returned `&[Dep]` degenerates to an empty slice on that arm
1693 /// without any silent `None` collapse).
1694 ///
1695 /// The `:deps-dev` slot carries the universal-axis dev-only
1696 /// dependency list every kind of caixa emits under (CAIXA-SDLC §I —
1697 /// the author-facing sibling of `:deps` that every `defcaixa` form
1698 /// supplies to declare tests / lint / bench closures the runtime
1699 /// `:deps` axis does not carry; the substrate-wide dev-closure-input
1700 /// axis every downstream test-facing artifact emits under, matching
1701 /// Cargo's `[dev-dependencies]` table's dev-time-only visibility
1702 /// contract) — the typed slot's `Vec<Dep>` accept-set (empty-`:nome`
1703 /// rejected through [`DepError::NomeEmpty`], non-DNS-1123-label
1704 /// `:nome` rejected through [`DepError::NomeInvalid`], malformed
1705 /// `:versao` rejected through [`DepError::VersaoInvalid`], empty
1706 /// `:fonte.repo` rejected through [`DepError::FonteRepoEmpty`],
1707 /// within-list duplicate `:nome` rejected through
1708 /// [`DepError::DuplicateNome { list: ":deps-dev" }`]) maps onto every
1709 /// load-bearing downstream consumer the substrate carries — the
1710 /// [`Self::validate_deps`] per-entry [`Dep::validate`] + within-list
1711 /// dedup walk at caixa-core/src/manifest.rs, the
1712 /// [`crate::dep::validate_no_self_dep`] cross-list self-reference
1713 /// gate at caixa-core/src/layout.rs that checks each entry against
1714 /// the caixa's own `:nome`, the caixa-resolver
1715 /// `for dep in &root.deps_dev` closure walk at
1716 /// caixa-resolver/src/resolve.rs that seeds every dev-only git-clone
1717 /// target through the resolver's [`crate::Dep`]-keyed pipeline, and
1718 /// every future per-`Caixa` resolver-facing renderer the CAIXA-SDLC
1719 /// §I roadmap acknowledges (the future per-cluster dev-closure-audit
1720 /// overlay the M4 CR materializer resolves per-CR, the future
1721 /// `lacre.lisp` BLAKE3-closure emit walk the caixa-resolver docstring
1722 /// roadmaps).
1723 ///
1724 /// Second outer top-level [`Caixa`] `&[Dep]`-return slice-accessor —
1725 /// closes the outer-`Caixa` dependency-slot `&[Dep]` sub-family the
1726 /// sibling [`Self::deps`] (ad34b4e) opened on. The two accessors
1727 /// jointly close the two-list dep-graph surface every downstream
1728 /// resolver-facing consumer keys off (runtime `:deps` +
1729 /// dev-only `:deps-dev`, the canonical Cargo-shaped dependency-table
1730 /// pair the [`Self::validate_deps`] gate already walks in canonical
1731 /// order). Peer of the closed outer-`Caixa` foreign-code-slot
1732 /// `&[String]` sub-family ([`Self::bibliotecas`] 8a36c23,
1733 /// [`Self::exe`] 65d9527, [`Self::servicos`] 611f78b) and the outer-
1734 /// `Caixa` universal-axis text-tag family ([`Self::autores`]
1735 /// b5d813f, [`Self::etiquetas`] 78c7d3c) — folds the "outer
1736 /// [`Caixa`] `&[T]` slice" projection pattern onto the sibling
1737 /// dev-dep composite-element axis (`Dep` composite, matching the
1738 /// [`Self::deps`] element type). Sibling in shape to the peer
1739 /// per-`:supervisor` [`crate::supervisor::SupervisorSpec::children`]
1740 /// (bc92bce), per-`:placement`
1741 /// [`crate::aplicacao::Placement::clusters`] (a6e18d7),
1742 /// per-`:membros` [`crate::aplicacao::AplicacaoSpec::membros`]
1743 /// (6c77e36), per-`:contratos`
1744 /// [`crate::aplicacao::AplicacaoSpec::contratos`] (0dcc926), and
1745 /// per-`:upgrade-from :instructions`
1746 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1747 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1748 /// typed-slot list axes, folded here to the outer top-level
1749 /// [`Caixa`] universal-axis dev-dep-graph surface. Returns `&[Dep]`
1750 /// (not `&Vec<Dep>`) because every downstream consumer of the
1751 /// dev-dep list treats it as a read-only sequence — the slice-view
1752 /// is the narrowest borrow that supports every present +
1753 /// roadmapped consumer (`.iter()`, `.len()`, `.is_empty()`) without
1754 /// leaking the backing `Vec`'s grow/push/reserve surface no consumer
1755 /// of the typed view reaches for (the storage-side `Vec` remains
1756 /// reachable through the `pub deps_dev` field for the mutation-
1757 /// carrying serde round-trip and per-test fixture-mutation paths).
1758 /// Named `deps_dev()` to match the storage field's `snake_case` name;
1759 /// the kebab-case author-surface tag `:deps-dev` is the same axis
1760 /// after tatara-lisp's kebab↔snake fold and the accessor's identity
1761 /// maps onto the canonical CAIXA-SDLC §I vocabulary the slot's
1762 /// docstring already carries.
1763 #[must_use]
1764 pub fn deps_dev(&self) -> &[Dep] {
1765 self.deps_dev.as_slice()
1766 }
1767
1768 /// Substrate-canonical per-[`Caixa`] typed-dispatch read accessor
1769 /// every consumer that walks one of the two dep-list axes keyed on a
1770 /// [`crate::dep::DepList`] discriminant reaches for — routes the
1771 /// `(list: DepList) -> &[Dep]` projection through one typed method on
1772 /// the substrate primitive rather than the prior open-coded
1773 /// `match list { Prod => caixa.deps(), Dev => caixa.deps_dev() }`
1774 /// inline dispatch every per-axis walker would otherwise carry.
1775 /// Returns the author-declared per-list `Vec<Dep>` verbatim as a
1776 /// `&[Dep]` slice-view over the same backing buffer the sibling
1777 /// [`Self::deps`] (`Prod`) / [`Self::deps_dev`] (`Dev`) per-slot
1778 /// accessors borrow from, preserving the empty-list-carrying invariant
1779 /// each per-slot accessor already establishes (`:deps` / `:deps-dev`
1780 /// are default-empty axes every `defcaixa` form supplies with an empty
1781 /// `()` when unset; the [`Self::from_lisp`] derive folds an omitted
1782 /// list through `#[serde(default)]` to `Vec::new()`, so both arms
1783 /// definitionally carry a `Vec<Dep>` slot — possibly empty — and the
1784 /// returned `&[Dep]` degenerates to an empty slice on either arm
1785 /// without any silent `None` collapse).
1786 ///
1787 /// The [`crate::dep::DepList`] closed-set typed enum is the
1788 /// substrate's canonical discriminator for the "runtime-closure
1789 /// `:deps` vs dev-only-closure `:deps-dev`" axis every dep-list
1790 /// consumer dispatches on — the compiler-checked exhaustiveness on
1791 /// the enum's `match` arms is the build-time guarantee that no future
1792 /// per-list read-site regresses to a bare-`bool`-flag inline dispatch
1793 /// that a future third dep-list axis (a `:deps-build` build-only
1794 /// closure once the substrate grows cross-artifact heterogeneous
1795 /// dep-graphs, per CAIXA-SDLC §I) would silently split at every
1796 /// consumer. Prior to this the read side carried two per-slot
1797 /// accessors ([`Self::deps`] ad34b4e, [`Self::deps_dev`]) and no
1798 /// typed dispatch that a per-axis walker could parametrise on, so
1799 /// every per-list walker (the [`Self::validate_deps`] per-list
1800 /// [`crate::render::insert_first_seen`] dedup walk, a future
1801 /// `feira app graph` per-list dep summary, a future M4 per-cluster
1802 /// dev-closure-audit overlay the CR materializer resolves per-CR)
1803 /// open-coded the same two-block "run over `:deps`, then run over
1804 /// `:deps-dev`" pattern — a silent duplication that a future third
1805 /// dep-list axis would have had to grow a third block at every site.
1806 ///
1807 /// Peer of the sibling [`Self::push_dep`] typed-mutation dispatch
1808 /// (359fba5) — closes the two-side dispatch symmetry on the outer
1809 /// [`Caixa`] two-list dep-graph surface: `push_dep` on the mutation
1810 /// side, `deps_of` on the read side, both keyed on the same
1811 /// [`crate::dep::DepList`] discriminator. Same "one typed dispatch on
1812 /// the substrate primitive, thin projections at each consumer"
1813 /// discipline the sibling per-slot read accessors ([`Self::nome`]
1814 /// e6b7d97, [`Self::versao`], [`Self::kind`]) carry — extended onto
1815 /// the outer-[`Caixa`] typed-dispatch read surface.
1816 #[must_use]
1817 pub fn deps_of(&self, list: crate::dep::DepList) -> &[Dep] {
1818 match list {
1819 crate::dep::DepList::Prod => self.deps(),
1820 crate::dep::DepList::Dev => self.deps_dev(),
1821 }
1822 }
1823
1824 /// Substrate-canonical per-[`Caixa`] typed-mutation dispatch every
1825 /// consumer that appends to one of the two dep-list axes keys off
1826 /// — routes the `(list: DepList, dep: Dep)` tuple through one typed
1827 /// method on the substrate primitive rather than the prior
1828 /// `feira add`-side open-coded `if self.dev { &mut caixa.deps_dev }
1829 /// else { &mut caixa.deps }` inline dispatch + open-coded
1830 /// `.iter().any(|d| d.nome == …)` dup-check cascade. Refuses the
1831 /// mutation with the canonical typed [`DepError::DuplicateNome`] on
1832 /// a within-list name collision — the same `list: &'static str`
1833 /// diagnostic shape [`Self::validate_deps`]'s per-list
1834 /// [`crate::render::insert_first_seen`] walk raises on the peer
1835 /// parse-time within-list dedup axis, so a future author reading a
1836 /// `feira add` refusal and a `feira build` refusal reaches for the
1837 /// same corrective surface without switching diagnostic idioms.
1838 ///
1839 /// The two-arm [`crate::dep::DepList`] enum is the substrate's
1840 /// closed-set typed carrier for the "runtime-closure `:deps` vs
1841 /// dev-only-closure `:deps-dev`" axis every dep-list consumer
1842 /// dispatches on — the compiler-checked exhaustiveness on the
1843 /// enum's `match` arms is the build-time guarantee that no future
1844 /// per-list mutation-site regresses to a bare-`bool`-flag
1845 /// (`is_dev: bool`) inline dispatch that a future third
1846 /// dep-list axis (a `:deps-build` build-only closure once the
1847 /// substrate grows cross-artifact heterogeneous dep-graphs, per
1848 /// CAIXA-SDLC §I) would silently split at every consumer.
1849 ///
1850 /// Same "one typed dispatch on the substrate primitive, thin
1851 /// projections at each consumer" discipline the sibling per-slot
1852 /// read accessors ([`Self::deps`] ad34b4e, [`Self::deps_dev`],
1853 /// [`Self::nome`] e6b7d97, [`Self::versao`], [`Self::kind`])
1854 /// carry — extended onto the outer-[`Caixa`] typed-mutation surface,
1855 /// the substrate's first typed-mutation dispatch on the top-level
1856 /// manifest. The prior `feira add` open-coded `&mut caixa.deps` /
1857 /// `&mut caixa.deps_dev` inline field-access + `bail!` string-
1858 /// diagnostic path routed no through-line back to the typed slot,
1859 /// so a future extension of either dep-list axis to a richer author
1860 /// surface (a per-cluster override the operator pins through a
1861 /// future `:placement`-scoped dep-list slot the CAIXA-SDLC §I
1862 /// roadmap acknowledges, an M4
1863 /// `mesh.pleme.io/v1alpha1/Caixa` CR materializer's per-CR
1864 /// admission-webhook that normalized the list at admission time)
1865 /// would have had to be threaded through the `feira add` mutation
1866 /// site in lockstep with every read consumer or one path would
1867 /// silently disagree with the other on which list a given dep lands
1868 /// in. Lifting the resolution rule to a typed method on the
1869 /// substrate primitive means every downstream dep-list-mutating
1870 /// consumer of the top-level manifest reaches for exactly one typed
1871 /// dispatch — the resolver's accept-set migrates as a unit on any
1872 /// future axis addition.
1873 ///
1874 /// # Errors
1875 ///
1876 /// Returns [`DepError::DuplicateNome`] with `list = list.as_str()`
1877 /// when another entry in the same list already carries the same
1878 /// `:nome` — the mutation is refused and the caller can surface the
1879 /// typed diagnostic to the author (the `feira add` verb routes the
1880 /// error through `anyhow::Error::from`, which preserves the
1881 /// canonical `#[error(...)]`-templated diagnostic body).
1882 pub fn push_dep(&mut self, list: crate::dep::DepList, dep: Dep) -> Result<(), DepError> {
1883 let target = match list {
1884 crate::dep::DepList::Prod => &mut self.deps,
1885 crate::dep::DepList::Dev => &mut self.deps_dev,
1886 };
1887 if target.iter().any(|d| d.nome() == dep.nome()) {
1888 return Err(DepError::DuplicateNome {
1889 nome: dep.nome().to_string(),
1890 list: list.as_str(),
1891 });
1892 }
1893 target.push(dep);
1894 Ok(())
1895 }
1896
1897 /// Substrate-canonical per-`Caixa` `:limits` M2 typed-slot outer-
1898 /// composite Lunatic-per-process wasm32-sandboxing-composite optional-
1899 /// composite-reference accessor every consumer of the top-level
1900 /// manifest's per-Servico [`LimitsSpec`] outer-composite reader keys
1901 /// off — returns the author-declared `:limits` typed composite
1902 /// verbatim as an `Option<&LimitsSpec>` reference over the same
1903 /// backing storage the raw `self.limits.as_ref()` field access
1904 /// borrows from, with `None` naming the "no `:limits` block
1905 /// authored — every per-axis Lunatic-sandbox cap defers to the
1906 /// wasm-engine-default arm named on the per-axis
1907 /// [`LimitsSpec::memory`] / [`LimitsSpec::fuel`] /
1908 /// [`LimitsSpec::wall_clock`] / [`LimitsSpec::cpu`] scalar-accessor
1909 /// docstrings" partition every downstream Servico-M2-overlay
1910 /// emitter treats as "emit nothing" and the sibling
1911 /// [`crate::StandardLayout::verify`] per-`:limits` shape gate
1912 /// treats as "skip the per-axis
1913 /// [`crate::LimitsError::MemoryZero`] / `MemoryBelowWasm32Page` /
1914 /// `FuelZero` / `WallClockZero` / `CpuZero` refusal cascade".
1915 ///
1916 /// The outer `:limits` slot carries the M2 Servico-runtime typed
1917 /// composite — the load-bearing container of every Lunatic-shaped
1918 /// per-process wasm32-sandbox cap axis every long-running wasm
1919 /// component's runtime dispatches on (INSPIRATIONS §III.1 —
1920 /// Lunatic per-process linear-memory / fuel / wall-clock /
1921 /// millicore cap primitives translated onto pleme-io's typed
1922 /// `:limits :memory` / `:limits :fuel` / `:limits :wall-clock` /
1923 /// `:limits :cpu` sub-slot axes; CAIXA-SDLC §II — the typed-M2
1924 /// slot algebra the wasm-engine + `pleme-computeunit` Helm-library
1925 /// chart both fan on). Every per-`:limits` axis threads through a
1926 /// lifted per-slot accessor on the [`LimitsSpec`] type: the
1927 /// [`LimitsSpec::memory`] wasm32 linear-memory byte-cap scalar
1928 /// accessor, the [`LimitsSpec::fuel`] wasmtime fuel-cap scalar
1929 /// accessor, the [`LimitsSpec::wall_clock`] per-call wall-clock
1930 /// deadline scalar accessor, and the [`LimitsSpec::cpu`]
1931 /// K8s-millicore soft-CPU-share scalar accessor. Every downstream
1932 /// consumer that reaches for a limits axis first passes through
1933 /// this outer accessor onto the composite and then dispatches
1934 /// onto the per-axis accessor — the two-level dispatch means
1935 /// every per-`:limits` reader now routes through a typed dispatch
1936 /// on the substrate primitive at both altitudes.
1937 ///
1938 /// Prior to this lift the `.limits` `Option<LimitsSpec>` composite
1939 /// was accessed inline at three production sites — the
1940 /// [`crate::StandardLayout::verify`] per-`:limits` shape gate's
1941 /// `if let Some(l) = &caixa.limits { … }` traversal head
1942 /// (caixa-core/src/layout.rs:882, which drives the per-axis
1943 /// refusal cascade on the composite: the `LimitsError::MemoryZero`
1944 /// / `MemoryBelowWasm32Page` / `MemoryExceedsWasm32Max` /
1945 /// `FuelZero` / `FuelExceedsMax` / `WallClockZero` /
1946 /// `WallClockExceedsMax` / `CpuZero` / `CpuExceedsMax` refusals
1947 /// [`LimitsSpec::validate`] fans onto), the
1948 /// [`crate::render::servico_m2_overlay`] per-Servico M2 overlay
1949 /// emitter's `if let Some(limits) = &caixa.limits { … }` traversal
1950 /// head (caixa-core/src/render.rs:18504, which drives the
1951 /// `M2_KEY_LIMITS`-keyed `limits.is_empty()`-gated `serde_yaml`
1952 /// projection every `caixa-helm` / `caixa-flux` Servico values-
1953 /// block emitter fans on), and the
1954 /// [`Self::declared_servico_slots`] per-Servico M2 declared-slot-
1955 /// set enumerator's `self.limits.is_some()` presence probe
1956 /// (caixa-core/src/manifest.rs:1788, which drives the
1957 /// `M2_AUTHOR_KEY_LIMITS` kebab-case author-label push every
1958 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] kind-coherence
1959 /// gate reads) — three open-coded outer-field accesses that
1960 /// expressed no compile-time link back to the typed slot at the
1961 /// [`Caixa`] altitude. A future extension of the `:limits` outer
1962 /// axis to a richer author surface (a multi-`:limits` list the M4
1963 /// CR materializer resolves per-CR at admission time so a Servico
1964 /// can expose a compute-heavy + IO-heavy limits pair, a per-
1965 /// cluster `:limits-overrides` slot the operator pins so a
1966 /// cluster-specific policy can tighten a caixa-declared cap
1967 /// without re-authoring the `caixa.lisp`, a promotion of the
1968 /// plain `Option<LimitsSpec>` to a richer
1969 /// `{static, dynamic}` partition once the wasm-engine's runtime-
1970 /// resolved dynamic-cap surface lands) would have had to be
1971 /// threaded through all three open-coded copies in lockstep or
1972 /// one consumer would silently disagree with the peers on which
1973 /// limits composite a given Caixa resolves to — the layout gate's
1974 /// per-axis bracket-dispatch seed reading the raw slot while the
1975 /// peer `servico_m2_overlay` emitter read an operator-resolved
1976 /// slot would silently split the build-time sandbox-shape gate
1977 /// from the runtime `ComputeUnit` CR emission gate, a three-
1978 /// consumer split at the layout gate, the M2 overlay emitter, and
1979 /// the declared-slot enumerator far from the source `caixa.lisp`
1980 /// with no field naming the limits-drift root cause. Lifting the
1981 /// resolution rule to a typed method on the substrate primitive
1982 /// means every downstream consumer of the caixa's per-`Caixa`
1983 /// Lunatic-sandboxing outer-composite surface reaches for exactly
1984 /// one typed dispatch — the resolver's accept-set migrates as a
1985 /// unit on any future axis addition.
1986 ///
1987 /// First outer top-level [`Caixa`] `Option<&Composite>`-return
1988 /// composite-reference accessor — opens the outer-`Caixa`
1989 /// `Option<&Composite>` composite-reference projection pattern the
1990 /// sibling per-`Caixa` `:behavior` [`crate::BehaviorSpec`] /
1991 /// `:politicas` [`crate::aplicacao::MeshPolicy`] / `:placement`
1992 /// [`crate::aplicacao::Placement`] / `:entrada`
1993 /// [`crate::aplicacao::Entrada`] future outer-composite lifts
1994 /// fold on. Peer of the M3 mesh-slot outer-composite family the
1995 /// sibling [`crate::AplicacaoSpec::politicas`] (534dc21) /
1996 /// [`crate::AplicacaoSpec::placement`] (9abb8f0) /
1997 /// [`crate::AplicacaoSpec::entrada`] (d32111c) composite-reference
1998 /// accessors already close on the outer [`crate::AplicacaoSpec`]
1999 /// altitude — extends that "one typed dispatch on the substrate
2000 /// primitive, thin projections at each consumer" discipline onto
2001 /// the outer top-level [`Caixa`] altitude, opening the M2 Servico-
2002 /// runtime slot family's outer-composite axis. Returns
2003 /// `Option<&LimitsSpec>` (not the owning composite by copy or
2004 /// clone) because every downstream consumer of the limits
2005 /// composite treats it as a read-only per-axis dispatch source —
2006 /// the reference-view is the narrowest borrow that supports every
2007 /// present + roadmapped consumer (per-axis accessor dispatch,
2008 /// `.is_empty()`-gated overlay projection, presence-probe early
2009 /// return on the "author-omitted `:limits` ⇒ engine-default
2010 /// applies" partition) without cloning the composite through
2011 /// every consumer's fast path. The `Option` half of the return-
2012 /// type preserves the load-bearing "author-omitted `:limits` ⇒
2013 /// engine-default applies" partition (not a default composite the
2014 /// downstream must reject on emptiness) — the accessor projects
2015 /// the raw `Option<LimitsSpec>` slot's presence bit through the
2016 /// reference-return unchanged. Named `limits()` to match the
2017 /// storage field's name verbatim and the tatara-lisp author-
2018 /// surface term (`:limits`) the field's own docstring already
2019 /// carries.
2020 #[must_use]
2021 pub fn limits(&self) -> Option<&LimitsSpec> {
2022 self.limits.as_ref()
2023 }
2024
2025 /// Substrate-canonical per-`Caixa` `:behavior` M2 typed-slot outer-
2026 /// composite OTP-`gen_server`-shaped callback-table optional-
2027 /// composite-reference accessor every consumer of the top-level
2028 /// manifest's per-Servico [`BehaviorSpec`] outer-composite reader
2029 /// keys off — returns the author-declared `:behavior` typed
2030 /// composite verbatim as an `Option<&BehaviorSpec>` reference over
2031 /// the same backing storage the raw `self.behavior.as_ref()` field
2032 /// access borrows from, with `None` naming the "no `:behavior`
2033 /// block authored — every per-callback OTP-shaped hook defers to
2034 /// the wasm-engine's runtime default arm named on the per-axis
2035 /// [`BehaviorSpec::on_init`] / [`BehaviorSpec::on_call`] /
2036 /// [`BehaviorSpec::on_cast`] / [`BehaviorSpec::on_info`] /
2037 /// [`BehaviorSpec::on_state_change`] /
2038 /// [`BehaviorSpec::on_terminate`] scalar-accessor docstrings"
2039 /// partition every downstream Servico-M2-overlay emitter treats as
2040 /// "emit nothing" and the sibling [`crate::StandardLayout::verify`]
2041 /// per-`:behavior` shape gate treats as "skip the per-arm
2042 /// [`crate::behavior::BehaviorError`] refusal cascade + the
2043 /// per-callback on-disk `MissingEntry` existence check".
2044 ///
2045 /// The outer `:behavior` slot carries the M2 Servico-runtime typed
2046 /// composite — the load-bearing container of every OTP-shaped
2047 /// per-Servico lifecycle-callback path axis every long-running wasm
2048 /// component's runtime dispatches on (INSPIRATIONS §II.3 — Erlang/
2049 /// OTP `gen_server:init/1` / `handle_call/3` / `handle_cast/2` /
2050 /// `handle_info/2` / `code_change/3` / `terminate/2` primitives
2051 /// translated onto pleme-io's typed `:behavior :on-init` /
2052 /// `:on-call` / `:on-cast` / `:on-info` / `:on-state-change` /
2053 /// `:on-terminate` sub-slot axes; CAIXA-SDLC §II — the typed-M2
2054 /// slot algebra the wasm-engine + `pleme-computeunit` Helm-library
2055 /// chart both fan on). Every per-`:behavior` axis threads through a
2056 /// lifted per-callback accessor on the [`BehaviorSpec`] type
2057 /// (9b4ecde / d66c702 / 156ddbe / 99616ac / 4846cef / 701add7).
2058 /// Every downstream consumer that reaches for a behavior axis
2059 /// first passes through this outer accessor onto the composite
2060 /// and then dispatches onto the per-callback accessor — the
2061 /// two-level dispatch means every per-`:behavior` reader now
2062 /// routes through a typed dispatch on the substrate primitive at
2063 /// both altitudes.
2064 ///
2065 /// Composes cross-slot with the M2 `:upgrade-from` gate: the
2066 /// [`crate::upgrade::validate_upgrade_from_against_behavior`]
2067 /// cross-slot composition gate at [`crate::StandardLayout::verify`]
2068 /// keys the "per-version `:state-change` instruction must have a
2069 /// `:on-state-change` callback" precondition off this accessor's
2070 /// composite (the callback-side counterpart to the
2071 /// `:upgrade-from :instructions :state-change :script` refusal at
2072 /// the appup-side). Threading that gate's traversal input through
2073 /// this accessor closes the cross-slot invariant on the substrate
2074 /// primitive, not on the raw field.
2075 ///
2076 /// Prior to this lift the `.behavior` `Option<BehaviorSpec>`
2077 /// composite was accessed inline at four production sites — the
2078 /// [`crate::StandardLayout::verify`] per-`:behavior` shape gate's
2079 /// `if let Some(b) = &caixa.behavior { … }` traversal head
2080 /// (caixa-core/src/layout.rs:896, which drives the per-arm
2081 /// `BehaviorError` refusal cascade + the per-callback on-disk
2082 /// [`crate::LayoutError::MissingEntry`] existence check under
2083 /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK`]),
2084 /// the [`crate::upgrade::validate_upgrade_from_against_behavior`]
2085 /// cross-slot composition gate's `caixa.behavior.as_ref()`
2086 /// traversal-input feed (caixa-core/src/layout.rs:1008, which
2087 /// drives the `:state-change` ↔ `:on-state-change` precondition
2088 /// refusal), the [`crate::render::servico_m2_overlay`] per-Servico
2089 /// M2 overlay emitter's `if let Some(behavior) = &caixa.behavior
2090 /// { … }` traversal head (caixa-core/src/render.rs:18513, which
2091 /// drives the `M2_KEY_BEHAVIOR`-keyed `behavior.is_empty()`-gated
2092 /// `serde_yaml` projection every `caixa-helm` / `caixa-flux`
2093 /// Servico values-block emitter fans on), and the
2094 /// [`Self::declared_servico_slots`] per-Servico M2 declared-slot-
2095 /// set enumerator's `self.behavior.is_some()` presence probe
2096 /// (caixa-core/src/manifest.rs:1919, which drives the
2097 /// `M2_AUTHOR_KEY_BEHAVIOR` kebab-case author-label push every
2098 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] kind-coherence
2099 /// gate reads) — four open-coded outer-field accesses that
2100 /// expressed no compile-time link back to the typed slot at the
2101 /// [`Caixa`] altitude. A future extension of the `:behavior`
2102 /// outer axis to a richer author surface (a per-callback overlay
2103 /// resolver the operator materializes at admission time so a
2104 /// cluster-specific policy can inject a per-callback tracing
2105 /// interceptor without re-authoring the `caixa.lisp`, a promotion
2106 /// of the plain `Option<BehaviorSpec>` to a richer `{static,
2107 /// dynamic}` partition once a runtime-resolved behavior-swap
2108 /// surface lands, the M4 per-callback middleware chain the
2109 /// caixa-operator's per-Servico admission webhook keys off) would
2110 /// have had to be threaded through all four open-coded copies in
2111 /// lockstep or one consumer would silently disagree with the
2112 /// peers on which behavior composite a given Caixa resolves to —
2113 /// the layout gate's per-callback existence-check seed reading
2114 /// the raw slot while the peer `servico_m2_overlay` emitter read
2115 /// an operator-resolved slot would silently split the build-time
2116 /// callback-shape gate from the runtime `ComputeUnit` CR emission
2117 /// gate from the cross-slot `:state-change` composition gate from
2118 /// the M2 declared-slot enumerator, a four-consumer split far
2119 /// from the source `caixa.lisp` with no field naming the
2120 /// behavior-drift root cause. Lifting the resolution rule to a
2121 /// typed method on the substrate primitive means every downstream
2122 /// consumer of the caixa's per-`Caixa` OTP-callback-table outer-
2123 /// composite surface reaches for exactly one typed dispatch — the
2124 /// resolver's accept-set migrates as a unit on any future axis
2125 /// addition.
2126 ///
2127 /// Second outer top-level [`Caixa`] `Option<&Composite>`-return
2128 /// composite-reference accessor — sibling to the opening
2129 /// [`Self::limits`] (b2bd9d7) accessor on the outer-`Caixa`
2130 /// `Option<&Composite>` composite-reference sub-family, extends
2131 /// the "one typed dispatch on the substrate primitive, thin
2132 /// projections at each consumer" discipline onto the second of
2133 /// the three M2 Servico-runtime slots. The remaining
2134 /// `Option<&Composite>` axes at the outer top-level [`Caixa`]
2135 /// altitude — the M3 mesh-slot family (`:politicas`,
2136 /// `:placement`, `:entrada` — already closed on the inner
2137 /// [`crate::AplicacaoSpec`] altitude via 534dc21 / 9abb8f0 /
2138 /// d32111c) — remain the future sibling lifts on the outer
2139 /// top-level projection. Returns `Option<&BehaviorSpec>` (not
2140 /// the owning composite by copy or clone) because every
2141 /// downstream consumer of the behavior composite treats it as a
2142 /// read-only per-callback dispatch source — the reference-view is
2143 /// the narrowest borrow that supports every present + roadmapped
2144 /// consumer (per-callback accessor dispatch, `.is_empty()`-gated
2145 /// overlay projection, presence-probe early return on the
2146 /// "author-omitted `:behavior` ⇒ runtime-default applies"
2147 /// partition, cross-slot `:state-change` composition input)
2148 /// without cloning the composite through every consumer's fast
2149 /// path. The `Option` half of the return-type preserves the
2150 /// load-bearing "author-omitted `:behavior` ⇒ runtime-default
2151 /// applies" partition (not a default composite the downstream
2152 /// must reject on emptiness) — the accessor projects the raw
2153 /// `Option<BehaviorSpec>` slot's presence bit through the
2154 /// reference-return unchanged. Named `behavior()` to match the
2155 /// storage field's name verbatim and the tatara-lisp author-
2156 /// surface term (`:behavior`) the field's own docstring already
2157 /// carries.
2158 #[must_use]
2159 pub fn behavior(&self) -> Option<&crate::BehaviorSpec> {
2160 self.behavior.as_ref()
2161 }
2162
2163 /// Substrate-canonical per-`Caixa` `:politicas` M3 mesh-slot outer-
2164 /// composite MESH-COMPOSITION-shaped mesh-policy optional-composite-
2165 /// reference accessor every consumer of the top-level manifest's
2166 /// per-Aplicacao [`crate::aplicacao::MeshPolicy`] outer-composite
2167 /// reader keys off — returns the author-declared `:politicas` typed
2168 /// composite verbatim as an `Option<&MeshPolicy>` reference over the
2169 /// same backing storage the raw `self.politicas.as_ref()` field
2170 /// access borrows from, with `None` naming the "no `:politicas`
2171 /// block authored — every per-axis mesh-policy scalar defers to the
2172 /// cluster-default arm named on the per-axis
2173 /// [`crate::aplicacao::MeshPolicy::timeout`] /
2174 /// [`crate::aplicacao::MeshPolicy::retries`] /
2175 /// [`crate::aplicacao::MeshPolicy::circuit_breaker`] /
2176 /// [`crate::aplicacao::MeshPolicy::mtls_required`] /
2177 /// [`crate::aplicacao::MeshPolicy::rate_limit`] scalar-accessor
2178 /// docstrings" partition every downstream caixa-mesh /
2179 /// caixa-flux / caixa-helm Aplicacao-artifact emitter treats as
2180 /// "emit no per-`:politicas` overlay" and the sibling
2181 /// [`Self::aplicacao_view`] Aplicacao-composition seed folds through
2182 /// the [`crate::aplicacao::MeshPolicy::default`] cluster-default
2183 /// arm.
2184 ///
2185 /// The outer `:politicas` slot carries the M3 mesh-slot per-
2186 /// Aplicacao typed composite — the load-bearing container of every
2187 /// mesh-level policy axis every Cilium NetworkPolicy / Gateway API
2188 /// v1.x HTTPRoute / future M4 per-edge policy overlay emitter fans
2189 /// on (MESH-COMPOSITION §III.2 — the Aplicacao's typed mesh-policy
2190 /// composite; §V — the "no infinite blocking" per-call deadline +
2191 /// "sandboxing-by-default" mTLS-enforcement CSE invariants; §III.3
2192 /// — the typed inter-Servico contrato-edge overlay the per-`(:de,
2193 /// :para)` mesh renderer keys off). Every per-`:politicas` axis
2194 /// threads through a lifted per-slot accessor on the
2195 /// [`crate::aplicacao::MeshPolicy`] type: the
2196 /// [`crate::aplicacao::MeshPolicy::mtls_required`] (c0110f1) Cilium
2197 /// mTLS-enforcement toggle, the
2198 /// [`crate::aplicacao::MeshPolicy::retries`] (bdfb399) transient-
2199 /// failure retry budget, the [`crate::aplicacao::MeshPolicy::timeout`]
2200 /// (7073d0f) Gateway-API per-call deadline, the
2201 /// [`crate::aplicacao::MeshPolicy::circuit_breaker`] (b0e741a)
2202 /// Envoy-outlier-detection composite. Every downstream consumer
2203 /// that reaches for a mesh-policy axis first passes through this
2204 /// outer accessor onto the composite and then dispatches onto the
2205 /// per-axis accessor — the two-level dispatch means every per-
2206 /// `:politicas` reader now routes through a typed dispatch on the
2207 /// substrate primitive at both altitudes.
2208 ///
2209 /// Composes through [`Self::aplicacao_view`]'s Aplicacao-composition
2210 /// seed: the Aplicacao-view builder folds the outer `Option`'s
2211 /// author-omitted arm onto the [`crate::aplicacao::MeshPolicy::default`]
2212 /// cluster-default, so the peer inner [`crate::AplicacaoSpec::politicas`]
2213 /// (534dc21) `&MeshPolicy`-return accessor observes a typed
2214 /// composite whether or not the author declared the outer slot.
2215 /// The outer accessor preserves the "author-omitted vs authored-
2216 /// empty" partition the inner accessor's `is_empty()`-gated
2217 /// renderer overlay collapses — routing the presence bit through
2218 /// this accessor keeps the [`Self::declared_mesh_slots`] M3 kind-
2219 /// coherence enumerator's `M3_AUTHOR_KEY_POLITICAS` push separate
2220 /// from the inner `MeshPolicy::is_empty()`-gated overlay elision.
2221 ///
2222 /// Prior to this lift the `.politicas` `Option<MeshPolicy>`
2223 /// composite was accessed inline at two production sites — the
2224 /// [`Self::aplicacao_view`] Aplicacao-composition seed's
2225 /// `self.politicas.clone().unwrap_or_default()` traversal head
2226 /// (caixa-core/src/manifest.rs:1899, which drives the fold onto
2227 /// the [`crate::aplicacao::MeshPolicy::default`] cluster-default
2228 /// arm the inner [`crate::AplicacaoSpec::politicas`] accessor
2229 /// then observes), and the [`Self::declared_mesh_slots`] M3
2230 /// declared-slot-set enumerator's `self.politicas.is_some()`
2231 /// presence probe (caixa-core/src/manifest.rs:1961, which drives
2232 /// the `M3_AUTHOR_KEY_POLITICAS` kebab-case author-label push
2233 /// every [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
2234 /// coherence gate reads) — two open-coded outer-field accesses
2235 /// that expressed no compile-time link back to the typed slot at
2236 /// the [`Caixa`] altitude. A future extension of the `:politicas`
2237 /// outer axis to a richer author surface (a per-cluster
2238 /// `:politicas-overrides` slot the operator materializes at
2239 /// admission time so a cluster-specific policy can tighten the
2240 /// caixa-declared bound without re-authoring the `caixa.lisp`, a
2241 /// promotion of the plain `Option<MeshPolicy>` to a richer
2242 /// `{static, dynamic}` partition once the M4 per-edge
2243 /// contrato-scoped policy-override surface lands, the M5 traffic-
2244 /// shaping composition the caixa-operator's per-Aplicacao mesh
2245 /// admission webhook keys off) would have had to be threaded
2246 /// through both open-coded copies in lockstep or the Aplicacao-
2247 /// composition seed's default-fold arm would silently disagree
2248 /// with the M3 declared-slot enumerator on which policy composite
2249 /// a given Caixa resolves to — the seed reading an operator-
2250 /// resolved slot while the enumerator's presence probe read the
2251 /// raw slot would silently split the build-time mesh-artifact
2252 /// emission gate from the M3 declared-slot enumerator's kind-
2253 /// coherence gate, a two-consumer split far from the source
2254 /// `caixa.lisp` with no field naming the policy-drift root cause.
2255 /// Lifting the resolution rule to a typed method on the substrate
2256 /// primitive means every downstream consumer of the caixa's per-
2257 /// `Caixa` MESH-COMPOSITION mesh-policy outer-composite surface
2258 /// reaches for exactly one typed dispatch — the resolver's
2259 /// accept-set migrates as a unit on any future axis addition.
2260 ///
2261 /// Third outer top-level [`Caixa`] `Option<&Composite>`-return
2262 /// composite-reference accessor — sibling to the opening
2263 /// [`Self::limits`] (b2bd9d7) and [`Self::behavior`] (35d8b52)
2264 /// accessors on the outer-`Caixa` `Option<&Composite>` composite-
2265 /// reference sub-family, extends the "one typed dispatch on the
2266 /// substrate primitive, thin projections at each consumer"
2267 /// discipline onto the first of the three M3 mesh-slot axes.
2268 /// Peer of the closed inner mesh-slot outer-composite family the
2269 /// sibling [`crate::AplicacaoSpec::politicas`] (534dc21) /
2270 /// [`crate::AplicacaoSpec::placement`] (9abb8f0) /
2271 /// [`crate::AplicacaoSpec::entrada`] (d32111c) composite-reference
2272 /// accessor pins already close on the inner [`crate::AplicacaoSpec`]
2273 /// altitude — opens the outer top-level [`Caixa`] altitude's M3
2274 /// mesh-slot arm of the composite-reference family the remaining
2275 /// two axes (`:placement`, `:entrada`) fold onto in future
2276 /// sibling lifts. Returns `Option<&MeshPolicy>` (not the owning
2277 /// composite by copy or clone) because every downstream consumer
2278 /// of the mesh-policy composite treats it as a read-only per-axis
2279 /// dispatch source — the reference-view is the narrowest borrow
2280 /// that supports every present + roadmapped consumer (per-axis
2281 /// accessor dispatch, `.is_empty()`-gated overlay projection,
2282 /// presence-probe early return on the "author-omitted `:politicas`
2283 /// ⇒ cluster-default applies" partition, `Aplicacao`-composition
2284 /// seed's default-fold arm) without cloning the composite through
2285 /// every consumer's fast path. The `Option` half of the return-
2286 /// type preserves the load-bearing "author-omitted `:politicas` ⇒
2287 /// cluster-default applies" partition (not a default composite
2288 /// the downstream must reject on emptiness) — the accessor
2289 /// projects the raw `Option<MeshPolicy>` slot's presence bit
2290 /// through the reference-return unchanged. Named `politicas()` to
2291 /// match the storage field's name verbatim and the tatara-lisp
2292 /// author-surface term (`:politicas`) the field's own docstring
2293 /// already carries.
2294 #[must_use]
2295 pub fn politicas(&self) -> Option<&crate::aplicacao::MeshPolicy> {
2296 self.politicas.as_ref()
2297 }
2298
2299 /// Substrate-canonical per-`Caixa` `:placement` M3 mesh-slot outer-
2300 /// composite MESH-COMPOSITION-shaped distribution optional-composite-
2301 /// reference accessor every consumer of the top-level manifest's
2302 /// per-Aplicacao [`crate::aplicacao::Placement`] outer-composite
2303 /// reader keys off — returns the author-declared `:placement` typed
2304 /// composite verbatim as an `Option<&Placement>` reference over the
2305 /// same backing storage the raw `self.placement.as_ref()` field
2306 /// access borrows from, with `None` naming the "no `:placement`
2307 /// block authored — every per-axis placement scalar defers to the
2308 /// cluster-default arm named on the per-axis
2309 /// [`crate::aplicacao::Placement::estrategia`] /
2310 /// [`crate::aplicacao::Placement::clusters`] /
2311 /// [`crate::aplicacao::Placement::affinity`] /
2312 /// [`crate::aplicacao::Placement::shard_key`] scalar-accessor
2313 /// docstrings" partition every downstream caixa-mesh /
2314 /// caixa-flux / caixa-helm Aplicacao-artifact emitter treats as
2315 /// "emit no per-`:placement` overlay" and the sibling
2316 /// [`Self::aplicacao_view`] Aplicacao-composition seed folds through
2317 /// the [`crate::aplicacao::Placement::default`] cluster-default arm.
2318 ///
2319 /// The outer `:placement` slot carries the M3 mesh-slot per-
2320 /// Aplicacao typed distribution composite — the load-bearing
2321 /// container of every where-does-this-Aplicacao-run axis every
2322 /// caixa-mesh programs.yaml per-cluster distribution overlay /
2323 /// caixa-flux per-Aplicacao GitRepository/HelmRelease fan-out /
2324 /// future M4 per-Aplicacao Akka-style cluster-sharding entity-id
2325 /// resolver emitter fans on (MESH-COMPOSITION §II.4 — the
2326 /// Aplicacao's typed distribution composite; §V CSE invariants —
2327 /// "distribution is a first-class typed composite, not a runtime
2328 /// scheduler hint" the per-axis scalars enforce; §III.3 — the
2329 /// typed inter-Servico contrato-edge overlay the per-cluster
2330 /// mesh renderer keys off). Every per-`:placement` axis threads
2331 /// through a lifted per-slot accessor on the
2332 /// [`crate::aplicacao::Placement`] type: the
2333 /// [`crate::aplicacao::Placement::estrategia`] (921fe1b)
2334 /// MESH-COMPOSITION distribution-strategy scalar, the
2335 /// [`crate::aplicacao::Placement::clusters`] (a6e18d7) per-cluster
2336 /// distribution-target slice, the [`crate::aplicacao::Placement::affinity`]
2337 /// M3-Adaptive-compression-hint optional-scalar, and the
2338 /// [`crate::aplicacao::Placement::shard_key`] (7cd2a28) Akka-cluster-
2339 /// sharding extractor-expression optional-scalar. Every downstream
2340 /// consumer that reaches for a placement axis first passes through
2341 /// this outer accessor onto the composite and then dispatches onto
2342 /// the per-axis accessor — the two-level dispatch means every per-
2343 /// `:placement` reader now routes through a typed dispatch on the
2344 /// substrate primitive at both altitudes.
2345 ///
2346 /// Composes through [`Self::aplicacao_view`]'s Aplicacao-composition
2347 /// seed: the Aplicacao-view builder folds the outer `Option`'s
2348 /// author-omitted arm onto the [`crate::aplicacao::Placement::default`]
2349 /// cluster-default, so the peer inner [`crate::AplicacaoSpec::placement`]
2350 /// (9abb8f0) `&Placement`-return accessor observes a typed composite
2351 /// whether or not the author declared the outer slot. The outer
2352 /// accessor preserves the "author-omitted vs authored-empty" partition
2353 /// the inner accessor collapses at the cluster-default fold —
2354 /// routing the presence bit through this accessor keeps the
2355 /// [`Self::declared_mesh_slots`] M3 kind-coherence enumerator's
2356 /// `M3_AUTHOR_KEY_PLACEMENT` push separate from the inner
2357 /// [`crate::AplicacaoSpec::validate_placement`]-gated overlay
2358 /// dispatch.
2359 ///
2360 /// Prior to this lift the `.placement` `Option<Placement>`
2361 /// composite was accessed inline at two production sites — the
2362 /// [`Self::aplicacao_view`] Aplicacao-composition seed's
2363 /// `self.placement.clone().unwrap_or_default()` traversal head
2364 /// (caixa-core/src/manifest.rs:2036, which drives the fold onto
2365 /// the [`crate::aplicacao::Placement::default`] cluster-default
2366 /// arm the inner [`crate::AplicacaoSpec::placement`] accessor
2367 /// then observes), and the [`Self::declared_mesh_slots`] M3
2368 /// declared-slot-set enumerator's `self.placement.is_some()`
2369 /// presence probe (caixa-core/src/manifest.rs:2100, which drives
2370 /// the `M3_AUTHOR_KEY_PLACEMENT` kebab-case author-label push
2371 /// every [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
2372 /// coherence gate reads) — two open-coded outer-field accesses
2373 /// that expressed no compile-time link back to the typed slot at
2374 /// the [`Caixa`] altitude. A future extension of the `:placement`
2375 /// outer axis to a richer author surface (a per-cluster
2376 /// `:placement-overrides` slot the operator materializes at
2377 /// admission time so a cluster-specific placement can tighten the
2378 /// caixa-declared bound without re-authoring the `caixa.lisp`, a
2379 /// per-tenant placement-alias table the M4
2380 /// `mesh.pleme.io/v1alpha1/Aplicacao` CR materializer resolves
2381 /// per-CR at admission time, a promotion of the plain
2382 /// `Option<Placement>` to a richer `{static, dynamic}` partition
2383 /// once Orleans-style virtual-actor dynamic placement comes into
2384 /// typed scope) would have had to be threaded through both open-
2385 /// coded copies in lockstep or the Aplicacao-composition seed's
2386 /// default-fold arm would silently disagree with the M3 declared-
2387 /// slot enumerator on which distribution composite a given Caixa
2388 /// resolves to — the seed reading an operator-resolved slot while
2389 /// the enumerator's presence probe read the raw slot would
2390 /// silently split the build-time distribution-artifact emission
2391 /// gate from the M3 declared-slot enumerator's kind-coherence
2392 /// gate, a two-consumer split far from the source `caixa.lisp`
2393 /// with no field naming the distribution-drift root cause.
2394 /// Lifting the resolution rule to a typed method on the substrate
2395 /// primitive means every downstream consumer of the caixa's per-
2396 /// `Caixa` MESH-COMPOSITION distribution outer-composite surface
2397 /// reaches for exactly one typed dispatch — the resolver's
2398 /// accept-set migrates as a unit on any future axis addition.
2399 ///
2400 /// Fourth outer top-level [`Caixa`] `Option<&Composite>`-return
2401 /// composite-reference accessor — sibling to the opening
2402 /// [`Self::limits`] (b2bd9d7) / [`Self::behavior`] (35d8b52) M2-
2403 /// Servico-runtime pair and the peer [`Self::politicas`] (5d23d29)
2404 /// M3-mesh-slot arm on the outer-`Caixa` `Option<&Composite>`
2405 /// composite-reference sub-family, folds on the "one typed
2406 /// dispatch on the substrate primitive, thin projections at each
2407 /// consumer" discipline extended onto the second of the three M3
2408 /// mesh-slot axes. Peer of the closed inner mesh-slot outer-
2409 /// composite family the sibling
2410 /// [`crate::AplicacaoSpec::politicas`] (534dc21) /
2411 /// [`crate::AplicacaoSpec::placement`] (9abb8f0) /
2412 /// [`crate::AplicacaoSpec::entrada`] (d32111c) composite-reference
2413 /// accessor pins already close on the inner
2414 /// [`crate::AplicacaoSpec`] altitude — folds on the outer top-
2415 /// level [`Caixa`] altitude's M3 mesh-slot arm the sibling
2416 /// [`Self::politicas`] opened, extending the discipline onto the
2417 /// second of the three M3 mesh-slot axes. The remaining M3
2418 /// mesh-slot axis (`:entrada`) folds onto this accessor's
2419 /// discipline in the final sibling lift, closing the outer top-
2420 /// level [`Caixa`] `Option<&Composite>` M3 mesh-slot sub-family.
2421 /// Returns `Option<&Placement>` (not the owning composite by copy
2422 /// or clone) because every downstream consumer of the placement
2423 /// composite treats it as a read-only per-axis dispatch source —
2424 /// the reference-view is the narrowest borrow that supports every
2425 /// present + roadmapped consumer (per-axis accessor dispatch,
2426 /// serde composite-serialization on the programs.yaml overlay,
2427 /// presence-probe early return on the "author-omitted `:placement`
2428 /// ⇒ cluster-default applies" partition, `Aplicacao`-composition
2429 /// seed's default-fold arm) without cloning the composite through
2430 /// every consumer's fast path. The `Option` half of the return-
2431 /// type preserves the load-bearing "author-omitted `:placement` ⇒
2432 /// cluster-default applies" partition (not a default composite
2433 /// the downstream must reject on emptiness) — the accessor
2434 /// projects the raw `Option<Placement>` slot's presence bit
2435 /// through the reference-return unchanged. Named `placement()` to
2436 /// match the storage field's name verbatim and the tatara-lisp
2437 /// author-surface term (`:placement`) the field's own docstring
2438 /// already carries.
2439 #[must_use]
2440 pub fn placement(&self) -> Option<&crate::aplicacao::Placement> {
2441 self.placement.as_ref()
2442 }
2443
2444 /// Substrate-canonical per-`Caixa` `:entrada` M3 mesh-slot outer-
2445 /// composite MESH-COMPOSITION-shaped external-gateway optional-
2446 /// composite-reference accessor every consumer of the top-level
2447 /// manifest's per-Aplicacao [`crate::aplicacao::Entrada`] outer-
2448 /// composite reader keys off — returns the author-declared
2449 /// `:entrada` typed composite verbatim as an `Option<&Entrada>`
2450 /// reference over the same backing storage the raw
2451 /// `self.entrada.as_ref()` field access borrows from, with `None`
2452 /// naming the "no `:entrada` block authored — this Aplicacao is
2453 /// cluster-internal, no `Gateway`/`HTTPRoute` fan-out emitted"
2454 /// partition every downstream caixa-mesh Gateway-API artifact
2455 /// emitter treats as "emit no gateway-listener + no `HTTPRoute`
2456 /// backend for this Aplicacao" and the sibling
2457 /// [`Self::aplicacao_view`] Aplicacao-composition seed forwards
2458 /// verbatim (unlike the peer `:politicas` / `:placement` arms,
2459 /// `:entrada` has no cluster-default fold — an omitted `:entrada`
2460 /// stays `None` on the projected [`crate::AplicacaoSpec`] and the
2461 /// peer inner [`crate::AplicacaoSpec::entrada`] accessor observes
2462 /// the same `Option<&Entrada>` presence bit unchanged).
2463 ///
2464 /// The outer `:entrada` slot carries the M3 mesh-slot per-
2465 /// Aplicacao typed external-gateway composite — the load-bearing
2466 /// container of every how-does-the-outside-world-reach-this-
2467 /// Aplicacao axis every caixa-mesh `Gateway`/`HTTPRoute` fan-out
2468 /// emitter fans on (MESH-COMPOSITION §II.5 — the Aplicacao's typed
2469 /// external-entry composite; §V CSE invariants — "the external
2470 /// gateway is a first-class typed composite, not a per-Servico
2471 /// ingress annotation" the per-axis scalars enforce; §III.4 — the
2472 /// typed hostname + backend-Servico pair the per-cluster Gateway-
2473 /// API renderer keys off). Every per-`:entrada` axis threads
2474 /// through a lifted per-slot accessor on the
2475 /// [`crate::aplicacao::Entrada`] type: the
2476 /// [`crate::aplicacao::Entrada::host`] Gateway-API `Listener.hostname`
2477 /// scalar, the [`crate::aplicacao::Entrada::para`] backend-Servico
2478 /// caixa-name scalar, the [`crate::aplicacao::Entrada::paths`]
2479 /// per-rule `HTTPPathMatch` list, the [`crate::aplicacao::Entrada::port`]
2480 /// backend `trigger.service.port` scalar, and the
2481 /// [`crate::aplicacao::Entrada::resolved_paths`] URL-path fallback
2482 /// resolver every HTTPRoute-aware renderer consumes. Every
2483 /// downstream consumer that reaches for an entry axis first passes
2484 /// through this outer accessor onto the composite and then
2485 /// dispatches onto the per-axis accessor — the two-level dispatch
2486 /// means every per-`:entrada` reader now routes through a typed
2487 /// dispatch on the substrate primitive at both altitudes.
2488 ///
2489 /// Composes through [`Self::aplicacao_view`]'s Aplicacao-composition
2490 /// seed: the Aplicacao-view builder forwards the outer `Option`
2491 /// arm verbatim (no default fold — `:entrada` is inherently
2492 /// optional; a cluster-internal Aplicacao has no external gateway
2493 /// at all, not "an external gateway that defaults to nothing"), so
2494 /// the peer inner [`crate::AplicacaoSpec::entrada`] (d32111c)
2495 /// `Option<&Entrada>`-return accessor observes the same presence
2496 /// bit whether or not the author declared the outer slot. Routing
2497 /// the presence bit through this accessor keeps the
2498 /// [`Self::declared_mesh_slots`] M3 kind-coherence enumerator's
2499 /// `M3_AUTHOR_KEY_ENTRADA` push separate from the inner
2500 /// [`crate::AplicacaoSpec::validate_entrada`]-gated
2501 /// hostname/backend/path emission dispatch.
2502 ///
2503 /// Prior to this lift the `.entrada` `Option<Entrada>` composite
2504 /// was accessed inline at two production sites — the
2505 /// [`Self::aplicacao_view`] Aplicacao-composition seed's
2506 /// `self.entrada.clone()` traversal head (caixa-core/src/manifest.rs:2182,
2507 /// which drives the forward onto the peer inner
2508 /// [`crate::AplicacaoSpec::entrada`] accessor the caixa-mesh
2509 /// Gateway-API fan-out then observes), and the
2510 /// [`Self::declared_mesh_slots`] M3 declared-slot-set enumerator's
2511 /// `self.entrada.is_some()` presence probe (caixa-core/src/manifest.rs:2248,
2512 /// which drives the `M3_AUTHOR_KEY_ENTRADA` kebab-case author-
2513 /// label push every [`crate::LayoutError::MeshSlotsOnNonAplicacao`]
2514 /// kind-coherence gate reads) — two open-coded outer-field
2515 /// accesses that expressed no compile-time link back to the typed
2516 /// slot at the [`Caixa`] altitude. A future extension of the
2517 /// `:entrada` outer axis to a richer author surface (a per-cluster
2518 /// `:entrada-overrides` slot the operator materializes at admission
2519 /// time so a cluster-specific hostname can pin the caixa-declared
2520 /// bound without re-authoring the `caixa.lisp`, a per-tenant
2521 /// gateway-alias table the M4 `mesh.pleme.io/v1alpha1/Aplicacao`
2522 /// CR materializer resolves per-CR at admission time, a promotion
2523 /// of the plain `Option<Entrada>` to a richer
2524 /// `{public, private, internal}` partition once Cilium-identity-
2525 /// scoped internal gateways come into typed scope) would have had
2526 /// to be threaded through both open-coded copies in lockstep or the
2527 /// Aplicacao-composition seed's forward arm would silently
2528 /// disagree with the M3 declared-slot enumerator on which external-
2529 /// gateway composite a given Caixa resolves to — the seed reading
2530 /// an operator-resolved slot while the enumerator's presence probe
2531 /// read the raw slot would silently split the build-time gateway-
2532 /// artifact emission gate from the M3 declared-slot enumerator's
2533 /// kind-coherence gate, a two-consumer split far from the source
2534 /// `caixa.lisp` with no field naming the entry-drift root cause.
2535 /// Lifting the resolution rule to a typed method on the substrate
2536 /// primitive means every downstream consumer of the caixa's per-
2537 /// `Caixa` MESH-COMPOSITION external-gateway outer-composite
2538 /// surface reaches for exactly one typed dispatch — the resolver's
2539 /// accept-set migrates as a unit on any future axis addition.
2540 ///
2541 /// Fifth and final outer top-level [`Caixa`] `Option<&Composite>`-
2542 /// return composite-reference accessor — closes the outer-`Caixa`
2543 /// `Option<&Composite>` composite-reference sub-family opened by
2544 /// [`Self::limits`] (b2bd9d7) / [`Self::behavior`] (35d8b52) on the
2545 /// M2 Servico-runtime arm and extended onto the M3 mesh-slot arm
2546 /// by [`Self::politicas`] (5d23d29) / [`Self::placement`] (4fb8074),
2547 /// folds on the "one typed dispatch on the substrate primitive,
2548 /// thin projections at each consumer" discipline extended onto the
2549 /// third and final M3 mesh-slot axis. Peer of the closed inner
2550 /// mesh-slot outer-composite family the sibling
2551 /// [`crate::AplicacaoSpec::politicas`] (534dc21) /
2552 /// [`crate::AplicacaoSpec::placement`] (9abb8f0) /
2553 /// [`crate::AplicacaoSpec::entrada`] (d32111c) composite-reference
2554 /// accessor pins already close on the inner
2555 /// [`crate::AplicacaoSpec`] altitude — this lift closes the mirror
2556 /// sub-family on the outer top-level [`Caixa`] altitude, so both
2557 /// altitudes of the outer-composite reference-return discipline
2558 /// (per-`Caixa` outer-slot presence + per-`AplicacaoSpec` inner-
2559 /// slot presence) now carry the full five-arm accept-set behind a
2560 /// typed dispatch on the substrate primitive. Returns
2561 /// `Option<&Entrada>` (not the owning composite by copy or clone)
2562 /// because every downstream consumer of the entrada composite
2563 /// treats it as a read-only per-axis dispatch source — the
2564 /// reference-view is the narrowest borrow that supports every
2565 /// present + roadmapped consumer (per-axis accessor dispatch,
2566 /// serde composite-serialization on the programs.yaml overlay,
2567 /// presence-probe early return on the "author-omitted `:entrada`
2568 /// ⇒ cluster-internal Aplicacao" partition, `Aplicacao`-composition
2569 /// seed's forward arm) without cloning the composite through every
2570 /// consumer's fast path. The `Option` half of the return-type
2571 /// preserves the load-bearing "author-omitted `:entrada` ⇒
2572 /// cluster-internal Aplicacao" partition (not a default composite
2573 /// the downstream must reject on emptiness — a cluster-internal
2574 /// Aplicacao has no external gateway at all, not "a default gateway
2575 /// that emits nothing"); the accessor projects the raw
2576 /// `Option<Entrada>` slot's presence bit through the reference-
2577 /// return unchanged. Named `entrada()` to match the storage field's
2578 /// name verbatim and the tatara-lisp author-surface term
2579 /// (`:entrada`) the field's own docstring already carries.
2580 #[must_use]
2581 pub fn entrada(&self) -> Option<&crate::aplicacao::Entrada> {
2582 self.entrada.as_ref()
2583 }
2584
2585 /// Substrate-canonical per-`Caixa` `:ci` slot accessor — returns the
2586 /// author-declared typed CI run (`canteiro_types::CiRun`) verbatim as
2587 /// an `Option<&CiRun>`, borrowed from the typed slot's own
2588 /// `Option<CiRun>` storage. `None` when the slot is absent (every
2589 /// non-`Acao` kind, and an `Acao` caixa that hasn't declared `:ci`
2590 /// yet — the latter is caught by [`crate::LayoutError::MissingCi`],
2591 /// not silently accepted).
2592 ///
2593 /// Named `ci()` to match the storage field's name and the
2594 /// tatara-lisp author surface (`:ci`); mirrors the sibling
2595 /// `Option<&Composite>` accessors on this same `Caixa` altitude
2596 /// ([`Self::limits`], [`Self::behavior`], [`Self::politicas`],
2597 /// [`Self::placement`], [`Self::entrada`]) — one typed dispatch on
2598 /// the substrate primitive rather than an open-coded `self.ci.as_ref()`
2599 /// at every consumer.
2600 #[must_use]
2601 pub fn ci(&self) -> Option<&canteiro_types::CiRun> {
2602 self.ci.as_ref()
2603 }
2604
2605 /// Substrate-canonical per-`Caixa` `:estrategia` M2 supervisor-tree-
2606 /// slot flat-spread OTP-shaped sibling-restart-strategy discriminant
2607 /// accessor every consumer of the top-level manifest's per-Supervisor
2608 /// restart-strategy axis keys off — returns the author-declared
2609 /// `:estrategia` variant verbatim as an `Option<RestartStrategy>`,
2610 /// `Copy`-projected from the typed slot's own
2611 /// `Option<crate::supervisor::RestartStrategy>` storage. Optional
2612 /// (`:estrategia` is a flat-spread supervisor-only slot every
2613 /// non-`Supervisor`-kind `defcaixa` carries as `None` by
2614 /// `#[serde(default)]`, and every `Supervisor`-kind `defcaixa` may
2615 /// still omit to defer to [`RestartStrategy::default`] —
2616 /// [`RestartStrategy::OneForOne`] — through the [`Self::supervisor_view`]
2617 /// `unwrap_or_default()` fold; a returned `None` degenerates to the
2618 /// [`SupervisorSpec::default`]-inherited strategy without any silent
2619 /// promotion to a fresh explicit variant at the accessor boundary).
2620 ///
2621 /// The `:estrategia` slot carries the M2 typed OTP-shaped sibling-
2622 /// restart-strategy discriminant every substrate-side per-Supervisor
2623 /// dispatch fans on (INSPIRATIONS §II.2 — OTP `supervisor:strategy`
2624 /// closed-set `one_for_one | one_for_all | rest_for_one |
2625 /// simple_one_for_one` algebra translated onto pleme-io's typed
2626 /// [`RestartStrategy`] enum; CAIXA-SDLC §II — the M2 supervisor-tree
2627 /// slot algebra the operator's hierarchical reconciliation scheduler
2628 /// fans on). The slot is *flat-spread* on the outer top-level `Caixa`
2629 /// (per the field-shape docstring at caixa-core/src/manifest.rs — "The
2630 /// supervisor slots are flat on Caixa (vs nested under a
2631 /// `SupervisorSpec` sub-form) to keep tatara-lisp authoring at one
2632 /// level of nesting"), so the accessor's altitude is the outer
2633 /// [`Caixa`] surface rather than the composed [`SupervisorSpec`]
2634 /// altitude the sibling [`crate::supervisor::SupervisorSpec::estrategia`]
2635 /// (eafb619) accessor keys off. The two typed axes — the outer
2636 /// author-surface `Option<RestartStrategy>` on the [`Caixa`] altitude
2637 /// (author-omitted arm carried as `None`) and the inner post-
2638 /// composition `RestartStrategy` on the [`SupervisorSpec`] altitude
2639 /// (`Option` collapsed through the [`Self::supervisor_view`]
2640 /// `unwrap_or_default()` fold) — now share one accessor discipline for
2641 /// the shared substrate concept "the author-declared OTP-shaped
2642 /// sibling-restart-strategy variant that partitions the downstream
2643 /// per-Supervisor renderer's per-arm fan-out"; the outer-altitude
2644 /// `None` arm is the pre-composition presence bit every declared-slot
2645 /// enumerator ([`Self::declared_supervisor_slots`]) reads, and the
2646 /// inner-altitude non-`Option` `RestartStrategy` is the post-
2647 /// composition partition-dispatch input every strategy-arm consumer
2648 /// ([`SupervisorSpec::validate`], the future wasm-operator's per-
2649 /// Supervisor sibling-restart branch, the future M4
2650 /// `mesh.pleme.io/v1alpha1/Supervisor` CR materializer's admission
2651 /// webhook) fans on.
2652 ///
2653 /// Prior to this lift the `.estrategia` field was accessed inline at
2654 /// two production sites in `caixa-core/src/manifest.rs` — the
2655 /// [`Self::declared_supervisor_slots`] `SUPERVISOR_AUTHOR_KEY_ESTRATEGIA`
2656 /// presence-probe arm at `if self.estrategia.is_some()` (which drives
2657 /// the [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] kind-
2658 /// coherence gate's per-slot label push) and the [`Self::supervisor_view`]
2659 /// `SupervisorSpec` construction site at `estrategia:
2660 /// self.estrategia.unwrap_or_default()` (which composes the flat-
2661 /// spread outer author-surface `Option<RestartStrategy>` onto the
2662 /// inner post-composition [`SupervisorSpec`] `RestartStrategy` field
2663 /// the [`SupervisorSpec::estrategia`] accessor keys off) — two open-
2664 /// coded field-accesses that expressed no compile-time link back to
2665 /// the typed slot. A future extension of the outer `:estrategia` axis
2666 /// to a richer author surface (a per-cluster strategy override the
2667 /// operator pins through a future `:estrategia-overrides` overlay the
2668 /// MESH-COMPOSITION §III.2 supervision-canary roadmap acknowledges,
2669 /// a per-tenant strategy-alias table the M4 CR materializer resolves
2670 /// per-CR, a per-Supervisor dynamic strategy derivation the future
2671 /// adaptive-supervision engine computes from child-failure-history
2672 /// topology, a per-child-cohort strategy split the future
2673 /// `RestForCohort` extension the INSPIRATIONS.md §II.2 Erlang/OTP
2674 /// absorption roadmap acknowledges, a promotion of the plain
2675 /// `Option<RestartStrategy>` to a richer
2676 /// `AuthorDeclaredStrategy { declared, overlay }` newtype once the
2677 /// operator-resolved overlay lands) would have had to be threaded
2678 /// through both open-coded copies in lockstep or the enumerator's
2679 /// presence probe and the composition site's `unwrap_or_default()`
2680 /// fold would silently disagree on which strategy a given [`Caixa`]
2681 /// resolves to (an author's `:estrategia OneForAll` would satisfy
2682 /// the enumerator's presence probe while the composition site
2683 /// silently rendered a stale `OneForOne`, or vice versa). Lifting
2684 /// the resolution rule to a typed method on the substrate primitive
2685 /// means every downstream consumer of the caixa's per-`Caixa` outer-
2686 /// altitude sibling-restart-strategy surface reaches for exactly one
2687 /// typed dispatch — the resolver's accept-set migrates as a unit on
2688 /// any future axis addition.
2689 ///
2690 /// First outer top-level [`Caixa`] `Option<Copy>`-return supervisor-
2691 /// tree-slot flat-spread accessor for M2 supervisor-slot Copy-carry
2692 /// axes — opens the outer-`Caixa` `Option<Copy>` flat-spread
2693 /// projection pattern the sibling per-`Caixa` `:max-restarts`
2694 /// `Option<u32>` and (through the future duration-newtype landing)
2695 /// `:restart-window` `Option<Duration>` future outer-scalar lifts
2696 /// fold on. Peer of the inner-altitude [`crate::supervisor::SupervisorSpec::estrategia`]
2697 /// (eafb619) `Copy`-return sibling-restart-strategy scalar accessor on
2698 /// the post-composition [`SupervisorSpec`] altitude — same "one
2699 /// typed dispatch on the substrate primitive, thin projections at
2700 /// each consumer" discipline extended onto the pre-composition outer
2701 /// author-surface [`Caixa`] altitude for the same OTP-shaped
2702 /// sibling-restart-strategy axis. Peer of the closed outer-`Caixa`
2703 /// `Option<&Composite>` composite-reference family the sibling
2704 /// [`Self::limits`] (b2bd9d7) / [`Self::behavior`] (35d8b52) /
2705 /// [`Self::politicas`] (5d23d29) / [`Self::placement`] (4fb8074) /
2706 /// [`Self::entrada`] (e4128e4) accessor pins already carry on the
2707 /// outer `Option<&Composite>` altitude — extends the outer-`Caixa`
2708 /// typed-slot accessor discipline onto the flat-spread M2 supervisor-
2709 /// tree `Option<Copy>`-discriminant sub-family the sibling M3
2710 /// [`crate::aplicacao::Placement::estrategia`] (921fe1b)
2711 /// `PlacementStrategy` `Copy`-composite-enum scalar accessor already
2712 /// pins on the inner-altitude per-`:placement` composite. Named
2713 /// `estrategia()` to match the storage field's name and the
2714 /// per-[`SupervisorSpec`] peer [`crate::supervisor::SupervisorSpec::estrategia`]
2715 /// / per-[`crate::aplicacao::Placement`] peer
2716 /// [`crate::aplicacao::Placement::estrategia`] method-name discipline
2717 /// verbatim; the accessor's identity name maps onto the canonical
2718 /// OTP-shape supervision vocabulary the [`RestartStrategy`] enum's
2719 /// docstring already carries.
2720 #[must_use]
2721 pub fn estrategia(&self) -> Option<crate::supervisor::RestartStrategy> {
2722 self.estrategia
2723 }
2724
2725 /// Substrate-canonical per-`Caixa` `:max-restarts` M2 supervisor-tree-
2726 /// slot flat-spread OTP-`MaxIntensity`-shaped restart-budget-count
2727 /// scalar accessor every consumer of the top-level manifest's per-
2728 /// Supervisor `:max-restarts` restart-budget-count axis keys off —
2729 /// returns the author-declared `:max-restarts` typed `Option<u32>`
2730 /// verbatim, `Copy`-projected from the typed slot's own `Option<u32>`
2731 /// storage (`u32` is `Copy`, so `Option<u32>` is `Copy` and the
2732 /// accessor returns by value; no borrow of `&self` past the call).
2733 /// Optional (`:max-restarts` is a flat-spread supervisor-only slot
2734 /// every non-`Supervisor`-kind `defcaixa` carries as `None` by
2735 /// `#[serde(default)]`, and every `Supervisor`-kind `defcaixa` may
2736 /// still omit to defer to the [`Self::supervisor_view`]
2737 /// `unwrap_or(5)` fold's OTP-canonical `{intensity, 5, 60}` default).
2738 ///
2739 /// The `:max-restarts` slot carries the M2 typed Erlang/OTP-shaped
2740 /// `MaxIntensity` restart-budget count that pairs with the sibling
2741 /// `:restart-window` `Period` to form the `MaxIntensity / Period`
2742 /// restart-intensity ratio the supervisor trips its own escalation on
2743 /// (INSPIRATIONS §II.2 — Erlang/OTP `supervisor` `{intensity, 5, 60}`
2744 /// worker-supervisor default; RUNTIME-PATTERNS §II.2; CAIXA-SDLC §II
2745 /// — the M2 supervisor-tree slot algebra the operator's hierarchical
2746 /// reconciliation scheduler fans on). The slot is *flat-spread* on
2747 /// the outer top-level `Caixa` (per the field-shape docstring at
2748 /// caixa-core/src/manifest.rs — "The supervisor slots are flat on
2749 /// Caixa (vs nested under a `SupervisorSpec` sub-form)"), so the
2750 /// accessor's altitude is the outer [`Caixa`] surface rather than the
2751 /// composed [`SupervisorSpec`] altitude the sibling
2752 /// [`crate::supervisor::SupervisorSpec::max_restarts`] accessor keys
2753 /// off. The two typed axes — the outer author-surface `Option<u32>`
2754 /// on the [`Caixa`] altitude (author-omitted arm carried as `None`)
2755 /// and the inner post-composition `u32` on the [`SupervisorSpec`]
2756 /// altitude (`Option` collapsed through the [`Self::supervisor_view`]
2757 /// `unwrap_or(5)` fold) — now share one accessor discipline for the
2758 /// shared substrate concept "the author-declared OTP-shaped
2759 /// restart-budget count every downstream per-Supervisor consumer's
2760 /// restart-intensity budget-vs-count comparator fans on".
2761 ///
2762 /// Prior to this lift the `.max_restarts` field was accessed inline
2763 /// at two production sites in `caixa-core/src/manifest.rs` — the
2764 /// [`Self::declared_supervisor_slots`] `SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS`
2765 /// presence-probe arm at `if self.max_restarts.is_some()` (which
2766 /// drives the [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
2767 /// kind-coherence gate's per-slot label push) and the
2768 /// [`Self::supervisor_view`] `SupervisorSpec` construction site at
2769 /// `max_restarts: self.max_restarts.unwrap_or(5)` (which composes the
2770 /// flat-spread outer author-surface `Option<u32>` onto the inner
2771 /// post-composition [`SupervisorSpec`] `u32` field the
2772 /// [`SupervisorSpec::max_restarts`] accessor keys off) — two open-
2773 /// coded field-accesses that expressed no compile-time link back to
2774 /// the typed slot. A future extension of the outer `:max-restarts`
2775 /// axis to a richer author surface (a per-cluster restart-budget
2776 /// override the operator pins through a future `:max-restarts-overrides`
2777 /// overlay the MESH-COMPOSITION §III.2 supervision-canary roadmap
2778 /// acknowledges, a per-tenant restart-budget-alias table the M4 CR
2779 /// materializer resolves per-CR, a per-Supervisor dynamic restart-
2780 /// budget derivation the future adaptive-supervision engine computes
2781 /// from child-failure-history topology, a promotion of the plain
2782 /// `Option<u32>` count to a richer `{MaxR, MaxT}` per-child-cohort
2783 /// restart-budget-partition once the INSPIRATIONS §II.2 Erlang/OTP
2784 /// per-child-cohort roadmap lands) would have had to be threaded
2785 /// through both open-coded copies in lockstep or the enumerator's
2786 /// presence probe and the composition site's `unwrap_or(5)` fold
2787 /// would silently disagree on which restart-budget a given [`Caixa`]
2788 /// resolves to (an author's `:max-restarts 10` would satisfy the
2789 /// enumerator's presence probe while the composition site silently
2790 /// composed the OTP-canonical `5`, or vice versa). Lifting the
2791 /// resolution rule to a typed method on the substrate primitive means
2792 /// every downstream consumer of the caixa's per-`Caixa` outer-altitude
2793 /// restart-budget-count surface reaches for exactly one typed dispatch
2794 /// — the resolver's accept-set migrates as a unit on any future axis
2795 /// addition.
2796 ///
2797 /// Second outer top-level [`Caixa`] `Option<Copy>`-return supervisor-
2798 /// tree-slot flat-spread accessor for M2 supervisor-slot Copy-carry
2799 /// axes — folds on the outer-`Caixa` `Option<Copy>` flat-spread
2800 /// projection pattern the sibling per-`Caixa`
2801 /// [`Self::estrategia`] (ed04d3c) accessor opened, extends the
2802 /// sub-family onto the sibling `Option<u32>` restart-budget-count arm.
2803 /// Peer of the inner-altitude
2804 /// [`crate::supervisor::SupervisorSpec::max_restarts`] `u32` accessor
2805 /// on the post-composition [`SupervisorSpec`] altitude — same "one
2806 /// typed dispatch on the substrate primitive, thin projections at
2807 /// each consumer" discipline extended onto the pre-composition outer
2808 /// author-surface [`Caixa`] altitude for the same OTP-`MaxIntensity`-
2809 /// shaped restart-budget-count axis. Named `max_restarts()` to match
2810 /// the storage field's name and the per-[`SupervisorSpec`] peer
2811 /// [`crate::supervisor::SupervisorSpec::max_restarts`] method-name
2812 /// discipline verbatim; the accessor's identity maps onto the
2813 /// canonical OTP-shape supervision vocabulary the `:max-restarts`
2814 /// field's docstring already carries.
2815 #[must_use]
2816 pub const fn max_restarts(&self) -> Option<u32> {
2817 self.max_restarts
2818 }
2819
2820 /// Substrate-canonical per-`Caixa` `:restart-window` M2 supervisor-
2821 /// tree-slot flat-spread OTP-`Period`-shaped restart-intensity-
2822 /// denominator raw-duration-string scalar accessor every consumer of
2823 /// the top-level manifest's per-Supervisor `:restart-window` sliding-
2824 /// window axis keys off — returns the author-declared `:restart-window`
2825 /// typed `Option<String>` verbatim as an `Option<&str>`, borrowed
2826 /// from the typed slot's own `Option<String>` storage. `None` when
2827 /// the slot is absent (the canonical "never reset — every restart
2828 /// across the supervisor's lifetime counts against the sibling
2829 /// `:max-restarts` budget" sentinel every non-`Supervisor`-kind
2830 /// `defcaixa` carries by `#[serde(default)]` and every
2831 /// `Supervisor`-kind `defcaixa` may still omit to defer to the
2832 /// [`Self::supervisor_view`] `restart_window: None` composition
2833 /// through the [`crate::supervisor::duration_codec::parse`] soft-
2834 /// swallow `.and_then(|s| … .ok())` fold).
2835 ///
2836 /// The `:restart-window` slot carries the raw M2 typed Erlang/OTP-
2837 /// shaped `Period` sliding-observation-interval duration string that
2838 /// pairs with the sibling `:max-restarts` `MaxIntensity` restart-
2839 /// budget count to form the `MaxIntensity / Period` restart-intensity
2840 /// ratio the supervisor trips its own escalation on (INSPIRATIONS
2841 /// §II.2 — Erlang/OTP `supervisor` `{intensity, 5, 60}` worker-
2842 /// supervisor default; RUNTIME-PATTERNS §II.2). The outer-`Caixa`
2843 /// slot stores the raw duration string (`"60s"`, `"5m"`, `"500ms"`)
2844 /// authored under `:restart-window` — the typed [`SupervisorSpec`]
2845 /// holds an `Option<Duration>` routed through the shared
2846 /// [`crate::supervisor::duration_codec`] via `with = "duration_codec"`
2847 /// — so the outer altitude's accessor returns `Option<&str>` (raw
2848 /// authoring surface) while the inner altitude's
2849 /// [`crate::supervisor::SupervisorSpec::restart_window`] returns
2850 /// `Option<Duration>` (parsed typed surface). The parse-refusal arm
2851 /// is closed by the sibling [`Self::validate_restart_window`] gate
2852 /// that surfaces [`ManifestError::RestartWindowMalformed`] naming
2853 /// the offending value; the view-construction path
2854 /// [`Self::supervisor_view`] soft-swallows the same parse error to
2855 /// `None` to keep the view best-effort.
2856 ///
2857 /// Prior to this lift the `.restart_window` field was accessed inline
2858 /// at three production sites in `caixa-core/src/manifest.rs` — the
2859 /// [`Self::declared_supervisor_slots`]
2860 /// `SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW` presence-probe arm at
2861 /// `if self.restart_window.is_some()` (which drives the
2862 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] kind-
2863 /// coherence gate's per-slot label push), the
2864 /// [`Self::validate_restart_window`] `let Some(s) =
2865 /// self.restart_window.as_deref()` empty-and-shape gate binding
2866 /// (which folds the raw string through the shared
2867 /// [`crate::supervisor::duration_codec::parse`] to surface
2868 /// [`ManifestError::RestartWindowMalformed`] naming the offending
2869 /// value), and the [`Self::supervisor_view`] `self.restart_window
2870 /// .as_deref().and_then(…)` view-construction fold (which composes
2871 /// the flat-spread outer author-surface `Option<String>` onto the
2872 /// inner post-composition [`SupervisorSpec`] `Option<Duration>`
2873 /// field the [`SupervisorSpec::restart_window`] accessor keys off) —
2874 /// three open-coded field-accesses that expressed no compile-time
2875 /// link back to the typed slot. A future extension of the outer
2876 /// `:restart-window` axis to a richer author surface (a per-cluster
2877 /// window override, a per-tenant window-alias table, a per-Supervisor
2878 /// dynamic window derivation the future adaptive-supervision engine
2879 /// computes from child-failure-history topology, a promotion of the
2880 /// plain `Option<String>` raw duration to a typed `Option<Duration>`
2881 /// once the future author-surface parser lands at the [`Caixa`]
2882 /// altitude and the raw-string form is retired) would have had to be
2883 /// threaded through every open-coded copy in lockstep or the three
2884 /// consumers would silently disagree on which raw string a given
2885 /// [`Caixa`] resolves to. Lifting the resolution rule to a typed
2886 /// method on the substrate primitive means every downstream consumer
2887 /// of the caixa's per-`Caixa` outer-altitude restart-window raw-
2888 /// string surface reaches for exactly one typed dispatch — the
2889 /// resolver's accept-set migrates as a unit on any future axis
2890 /// addition.
2891 ///
2892 /// Third outer top-level [`Caixa`] supervisor-tree-slot flat-spread
2893 /// accessor — folds on the outer-`Caixa` M2 supervisor-tree flat-
2894 /// spread projection pattern the sibling per-`Caixa`
2895 /// [`Self::estrategia`] (ed04d3c) `Option<Copy>` and
2896 /// [`Self::max_restarts`] `Option<Copy>` accessors opened, extends
2897 /// the sub-family onto the sibling `Option<&str>` raw-duration-
2898 /// string arm (the outer altitude's raw-string form; the inner
2899 /// altitude's parsed [`Duration`] form is the peer
2900 /// [`crate::supervisor::SupervisorSpec::restart_window`] accessor).
2901 /// Peer of the sibling per-`Caixa` `Option<&str>`-return scalar
2902 /// accessors ([`Self::licenca`] / [`Self::repositorio`] /
2903 /// [`Self::descricao`] / [`Self::edicao`]) on the universal-axis
2904 /// outer scalar-projection family the outer-`Caixa` `Option<&str>`
2905 /// sub-family already carries — same "one typed dispatch on the
2906 /// substrate primitive, thin projections at each consumer"
2907 /// discipline extended onto the M2 supervisor-tree flat-spread
2908 /// `Option<&str>` raw-duration-string arm. Named `restart_window()`
2909 /// to match the storage field's name and the per-[`SupervisorSpec`]
2910 /// peer [`crate::supervisor::SupervisorSpec::restart_window`]
2911 /// method-name discipline verbatim; the accessor's identity maps
2912 /// onto the canonical OTP-shape supervision vocabulary the
2913 /// `:restart-window` field's docstring already carries.
2914 #[must_use]
2915 pub fn restart_window(&self) -> Option<&str> {
2916 self.restart_window.as_deref()
2917 }
2918
2919 /// Substrate-canonical per-`Caixa` `:upgrade-from` M2 typed-slot
2920 /// outer-composite OTP-appup-shaped per-prior-version migration-
2921 /// entry-list slice accessor every consumer of the top-level
2922 /// manifest's per-Servico hot-upgrade-block `&[UpgradeFromEntry]`
2923 /// slice-view keys off — returns the author-declared `:upgrade-from`
2924 /// typed `Vec<UpgradeFromEntry>` verbatim as a
2925 /// `&[UpgradeFromEntry]` slice-view over the same backing buffer
2926 /// the raw `self.upgrade_from.as_slice()` field access borrows
2927 /// from. Empty-slice-carrying (the "no hot-upgrade path declared"
2928 /// arm every `defcaixa` without an `:upgrade-from` block carries;
2929 /// the [`Self::from_lisp`] derive folds an omitted `:upgrade-from`
2930 /// through `#[serde(default)]` to `Vec::new()`, so a `Caixa` past
2931 /// parse definitionally carries a `Vec<UpgradeFromEntry>` slot —
2932 /// possibly empty — and the returned `&[UpgradeFromEntry]`
2933 /// degenerates to an empty slice on that arm without any silent
2934 /// `None` collapse).
2935 ///
2936 /// The outer `:upgrade-from` slot carries the M2 typed OTP-appup
2937 /// migration block — the load-bearing container of every per-
2938 /// prior-`:versao` migration-instruction list the wasm-operator
2939 /// dispatches on at hot-upgrade time (INSPIRATIONS §II.4 — OTP
2940 /// `.appup` per-prior-version `LoadModule | StateChange |
2941 /// SoftPurge | Purge | Restart` instruction algebra translated
2942 /// onto pleme-io's typed `:upgrade-from :from` + `:instructions`
2943 /// entry list; CAIXA-SDLC §II — the typed-M2 slot algebra the
2944 /// operator's hot-upgrade dispatch fans on). Every per-entry axis
2945 /// threads through a lifted per-entry accessor on the
2946 /// [`UpgradeFromEntry`] type: the
2947 /// [`UpgradeFromEntry::prior_versao`] SemVer-shaped previous-
2948 /// version scalar accessor and the
2949 /// [`UpgradeFromEntry::instructions`] `&[UpgradeInstruction]`-
2950 /// return per-entry instruction-list accessor (0137e5a). Every
2951 /// downstream consumer of the hot-upgrade path first passes
2952 /// through this outer accessor onto the slice and then dispatches
2953 /// per-entry through the inner accessors — the two-level dispatch
2954 /// means every per-`:upgrade-from` reader now routes through a
2955 /// typed dispatch on the substrate primitive at both altitudes.
2956 ///
2957 /// Prior to this lift the `.upgrade_from` `Vec<UpgradeFromEntry>`
2958 /// slot was accessed inline at production sites across three
2959 /// files — the [`Self::declared_servico_slots`] M2 declared-slot
2960 /// enumerator's `self.upgrade_from.is_empty()` presence probe
2961 /// (caixa-core/src/manifest.rs, which drives the
2962 /// `M2_AUTHOR_KEY_UPGRADE_FROM` kebab-case author-label push every
2963 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] kind-coherence
2964 /// gate reads), the [`crate::StandardLayout::verify`] per-
2965 /// `:upgrade-from` three-stage validation pass (caixa-core/src/
2966 /// layout.rs, which fans onto the
2967 /// [`crate::upgrade::validate_upgrade_from`] per-entry shape +
2968 /// cross-entry duplicate gate, the
2969 /// [`crate::upgrade::validate_upgrade_from_against_versao`]
2970 /// SemVer-precedence cross-slot gate, the
2971 /// [`crate::upgrade::validate_upgrade_from_against_behavior`]
2972 /// `:state-change` ↔ `:on-state-change` cross-slot composition
2973 /// gate, and the per-instruction script-path existence-probe walk
2974 /// that reads each entry's [`UpgradeFromEntry::instructions`] to
2975 /// resolve every declared migration script against the layout
2976 /// root), and the [`crate::render::servico_m2_overlay`] per-
2977 /// Servico M2 overlay emitter's `!caixa.upgrade_from.is_empty()`
2978 /// presence gate + `serde_yaml::to_value(&caixa.upgrade_from)`
2979 /// projection (caixa-core/src/render.rs, which drives the
2980 /// `M2_KEY_UPGRADE_FROM`-keyed `serde_yaml` projection every
2981 /// `caixa-helm` / `caixa-flux` Servico values-block emitter fans
2982 /// on and lands as the ComputeUnit CR's `spec.upgradeFrom` field).
2983 /// A future extension of the outer `:upgrade-from` axis (a per-
2984 /// cluster `:upgrade-overrides` overlay the wasm-engine operator
2985 /// resolves at admission time so a cluster-specific migration
2986 /// policy can tighten a caixa-declared step without re-authoring
2987 /// the `caixa.lisp`, promotion of the plain
2988 /// `Vec<UpgradeFromEntry>` to a richer `{static, dynamic}`
2989 /// partition once runtime-resolved hot-upgrade instructions land,
2990 /// per-entry priority annotation once multi-strategy fan-out
2991 /// lands) would have had to be threaded through all six open-
2992 /// coded copies in lockstep or one consumer would silently
2993 /// disagree with the peers on which upgrade slice a given Caixa
2994 /// resolves to — a six-consumer split at the enumerator, the
2995 /// three-stage validate pass, the script-path probe walk, and the
2996 /// M2 overlay emitter, far from the source `caixa.lisp` with no
2997 /// field naming the upgrade-drift root cause. Lifting the
2998 /// resolution rule to a typed method on the substrate primitive
2999 /// means every downstream consumer of the caixa's per-`Caixa`
3000 /// OTP-appup outer-slice surface reaches for exactly one typed
3001 /// dispatch — the resolver's accept-set migrates as a unit on any
3002 /// future axis addition.
3003 ///
3004 /// First outer top-level [`Caixa`] `&[Composite]`-return slice
3005 /// accessor for M2 / M3 typed-slot vec-carry axes — opens the
3006 /// outer-`Caixa` `&[Composite]` composite-slice projection
3007 /// pattern the sibling `:children`
3008 /// [`crate::supervisor::ChildSpec`] / `:membros`
3009 /// [`crate::aplicacao::Membro`] / `:contratos`
3010 /// [`crate::aplicacao::WitContract`] future outer-composite-slice
3011 /// lifts fold on. Peer of the closed outer-`Caixa` scalar
3012 /// `Option<&Composite>` composite-reference family the sibling
3013 /// [`Self::limits`] (b2bd9d7) / [`Self::behavior`] (35d8b52) /
3014 /// [`Self::politicas`] (5d23d29) / [`Self::placement`] (4fb8074) /
3015 /// [`Self::entrada`] (e4128e4) accessors closed on the outer
3016 /// `Option<&Composite>` altitude, extended here to the outer-
3017 /// `Caixa` `&[Composite]` vec-carry altitude. Peer at the inner
3018 /// altitude of [`crate::upgrade::UpgradeFromEntry::instructions`]
3019 /// (0137e5a) — same "one typed dispatch on the substrate
3020 /// primitive, thin projections at each consumer" discipline
3021 /// folded onto the outer top-level [`Caixa`] altitude, opening the
3022 /// M2 vec-carry slot family's outer-composite-slice axis. Sibling
3023 /// in shape to the peer outer-`Caixa` `&[Dep]`-return
3024 /// [`Self::deps`] (ad34b4e) / [`Self::deps_dev`] (f7fd81e) and
3025 /// `&[String]`-return [`Self::autores`] (b5d813f) /
3026 /// [`Self::etiquetas`] (78c7d3c) / [`Self::bibliotecas`]
3027 /// (8a36c23) / [`Self::exe`] (65d9527) / [`Self::servicos`]
3028 /// (611f78b) slice-accessors on the sibling outer-`Caixa` scalar-
3029 /// element vec-carry axes — folds the "outer [`Caixa`] `&[T]`
3030 /// slice" projection pattern onto the sibling M2 typed-composite-
3031 /// element axis (`UpgradeFromEntry` composite, matching the
3032 /// per-inner [`UpgradeFromEntry::instructions`] element type at a
3033 /// different altitude).
3034 ///
3035 /// Returns `&[UpgradeFromEntry]` (not `&Vec<UpgradeFromEntry>`)
3036 /// because every downstream consumer of the hot-upgrade list
3037 /// treats it as a read-only sequence — the slice-view is the
3038 /// narrowest borrow that supports every present + roadmapped
3039 /// consumer (`.iter()`, `.len()`, `.is_empty()`, `serde` slice-
3040 /// serialization through
3041 /// `serde_yaml::to_value(&[UpgradeFromEntry])`) without leaking
3042 /// the backing `Vec`'s grow/push/reserve surface no consumer of
3043 /// the typed view reaches for (the storage-side `Vec` remains
3044 /// reachable through the `pub upgrade_from` field for the
3045 /// mutation-carrying serde round-trip and per-test fixture-
3046 /// mutation paths). Named `upgrade_from()` to match the storage
3047 /// field's `snake_case` name; the kebab-case author-surface tag
3048 /// `:upgrade-from` is the same axis after tatara-lisp's
3049 /// kebab↔snake fold and the accessor's identity maps onto the
3050 /// canonical CAIXA-SDLC §II vocabulary the slot's docstring
3051 /// already carries.
3052 #[must_use]
3053 pub fn upgrade_from(&self) -> &[UpgradeFromEntry] {
3054 self.upgrade_from.as_slice()
3055 }
3056
3057 /// Substrate-canonical per-`Caixa` `:children` M2 supervisor-tree-
3058 /// slot outer-composite OTP-shaped per-supervisor static-child-list
3059 /// slice accessor every consumer of the top-level manifest's per-
3060 /// Supervisor `&[ChildSpec]` slice-view keys off — returns the
3061 /// author-declared `:children` typed `Vec<crate::supervisor::ChildSpec>`
3062 /// verbatim as a `&[crate::supervisor::ChildSpec]` slice-view over
3063 /// the same backing buffer the raw `self.children.as_slice()` field
3064 /// access borrows from. Empty-slice-carrying (the "no static children
3065 /// declared" arm every non-`Supervisor`-kind `defcaixa` carries by
3066 /// #[serde(default)] and every `SimpleOneForOne` supervisor carries
3067 /// by [`crate::supervisor::SupervisorError::SimpleOneForOneWithStaticChildren`]
3068 /// gate; the returned `&[ChildSpec]` degenerates to an empty slice
3069 /// on those arms without any silent `None` collapse).
3070 ///
3071 /// The outer `:children` slot carries the M2 typed OTP-supervisor
3072 /// static-child list — the load-bearing container of every per-
3073 /// child `{caixa, versao, restart}` triple the wasm-operator's
3074 /// hierarchical reconciler dispatches on at supervisor-tree
3075 /// materialization time (INSPIRATIONS §II.2 — OTP `supervisor:init/1`
3076 /// static-child list translated onto pleme-io's typed
3077 /// [`crate::supervisor::ChildSpec`] entry list; CAIXA-SDLC §II —
3078 /// the typed-M2 slot algebra the operator's per-supervisor fan-out
3079 /// dispatch fans on). Every per-child axis threads through a lifted
3080 /// per-entry accessor on the [`crate::supervisor::ChildSpec`] type:
3081 /// the [`crate::supervisor::ChildSpec::nome`] DNS-1123-label
3082 /// child-caixa-identity scalar accessor, the peer versao SemVer-2
3083 /// version-requirement scalar accessor, and the
3084 /// [`crate::supervisor::ChildSpec::restart`] `Copy`-composite-enum
3085 /// per-child post-exit restart-decision-policy discriminant
3086 /// accessor (dfb4a81). Every downstream consumer of the supervisor-
3087 /// tree path first passes through this outer accessor onto the
3088 /// slice and then dispatches per-child through the inner accessors
3089 /// — the two-level dispatch means every per-`:children` reader now
3090 /// routes through a typed dispatch on the substrate primitive at
3091 /// both altitudes.
3092 ///
3093 /// Prior to this lift the `.children` `Vec<ChildSpec>` slot was
3094 /// accessed inline at three production sites across two files —
3095 /// the [`Self::declared_supervisor_slots`] supervisor-tree
3096 /// declared-slot enumerator's `!self.children.is_empty()` presence
3097 /// probe (caixa-core/src/manifest.rs, which drives the
3098 /// `SUPERVISOR_AUTHOR_KEY_CHILDREN` kebab-case author-label push
3099 /// every [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
3100 /// kind-coherence gate reads), the [`Self::supervisor_view`]
3101 /// per-supervisor typed-view composer's `self.children.clone()`
3102 /// per-child fold-in path (caixa-core/src/manifest.rs, which
3103 /// materializes the typed [`crate::supervisor::SupervisorSpec`]
3104 /// view every [`crate::StandardLayout::verify`] Supervisor-arm gate
3105 /// dispatches on), and the [`crate::StandardLayout::verify`] per-
3106 /// `:children :caixa` self-parent refusal probe's
3107 /// `&caixa.children`-borrowed
3108 /// [`crate::supervisor::validate_no_self_supervision`] input
3109 /// (caixa-core/src/layout.rs, which pins the "no child names the
3110 /// supervisor's own `:nome`" cross-slot coherence gate). A future
3111 /// extension of the outer `:children` axis (a per-cluster
3112 /// `:children-overrides` overlay the wasm-engine operator resolves
3113 /// at admission time so a cluster-specific child-set can tighten
3114 /// a caixa-declared list without re-authoring the `caixa.lisp`,
3115 /// promotion of the plain `Vec<ChildSpec>` to a richer
3116 /// `{static, dynamic}` partition once Erlang/OTP's
3117 /// `simple_one_for_one`-shaped dynamic-child slot lands as a typed
3118 /// axis, per-child priority annotation once multi-strategy fan-out
3119 /// lands) would have had to be threaded through all three open-
3120 /// coded copies in lockstep or one consumer would silently
3121 /// disagree with the peers on which child slice a given Caixa
3122 /// resolves to — the enumerator's presence probe reading the raw
3123 /// slot while the peer view-composer's fold-in path read an
3124 /// operator-resolved slot would silently split the paired
3125 /// declared-slot enumerator and typed-view composition, and the
3126 /// [`crate::supervisor::validate_no_self_supervision`] self-parent
3127 /// refusal probe reading a third borrow would silently drift the
3128 /// cross-slot coherence gate's traversal input from the two peers,
3129 /// a three-consumer split at the enumerator, the view composer,
3130 /// and the self-parent gate far from the source `caixa.lisp` with
3131 /// no field naming the child-set-drift root cause. Lifting the
3132 /// resolution rule to a typed method on the substrate primitive
3133 /// means every downstream consumer of the caixa's per-`Caixa`
3134 /// OTP-supervisor outer-slice surface reaches for exactly one
3135 /// typed dispatch — the resolver's accept-set migrates as a unit
3136 /// on any future axis addition.
3137 ///
3138 /// Second outer top-level [`Caixa`] `&[Composite]`-return slice
3139 /// accessor for M2 / M3 typed-slot vec-carry axes — folds on the
3140 /// outer-`Caixa` `&[Composite]` composite-slice sub-family the
3141 /// sibling [`Self::upgrade_from`] (2a1f907) accessor opened, peer
3142 /// at the outer altitude of the closed inner-`SupervisorSpec`
3143 /// [`crate::SupervisorSpec::children`] (bc92bce) accessor on the
3144 /// same OTP-supervisor static-child-list axis — same "byte-equal,
3145 /// borrow-shared" outer-accessor discipline extended onto the
3146 /// second outer-`Caixa` `&[Composite]` vec-carry axis. Sibling in
3147 /// shape to the peer outer-`Caixa` `&[Dep]`-return [`Self::deps`]
3148 /// (ad34b4e) / [`Self::deps_dev`] (f7fd81e) and `&[String]`-return
3149 /// [`Self::autores`] (b5d813f) / [`Self::etiquetas`] (78c7d3c) /
3150 /// [`Self::bibliotecas`] (8a36c23) / [`Self::exe`] (65d9527) /
3151 /// [`Self::servicos`] (611f78b) slice-accessors on the sibling
3152 /// outer-`Caixa` scalar-element vec-carry axes — folds the "outer
3153 /// [`Caixa`] `&[T]` slice" projection pattern onto the sibling
3154 /// M2 typed-composite-element axis
3155 /// ([`crate::supervisor::ChildSpec`] composite, matching the
3156 /// per-inner [`crate::SupervisorSpec::children`] element type at a
3157 /// different altitude).
3158 ///
3159 /// Returns `&[crate::supervisor::ChildSpec]` (not
3160 /// `&Vec<ChildSpec>`) because every downstream consumer of the
3161 /// child list treats it as a read-only sequence — the slice-view
3162 /// is the narrowest borrow that supports every present +
3163 /// roadmapped consumer (`.iter()`, `.len()`, `.is_empty()`, the
3164 /// [`crate::supervisor::validate_no_self_supervision`] `&[ChildSpec]`
3165 /// input, `serde` slice-serialization) without leaking the backing
3166 /// `Vec`'s grow/push/reserve surface no consumer of the typed view
3167 /// reaches for (the storage-side `Vec` remains reachable through
3168 /// the `pub children` field for the mutation-carrying serde round-
3169 /// trip and per-test fixture-mutation paths, including the
3170 /// [`Self::supervisor_view`] fold-in path that clones the slot
3171 /// into the typed view). Named `children()` to match the storage
3172 /// field's name verbatim and the tatara-lisp author-surface term
3173 /// (`:children`) the field's own docstring already carries; the
3174 /// accessor's identity maps onto the canonical OTP supervision
3175 /// vocabulary the [`Caixa::children`] field's docstring already
3176 /// reaches for ("Static children of a supervisor").
3177 #[must_use]
3178 pub fn children(&self) -> &[crate::supervisor::ChildSpec] {
3179 self.children.as_slice()
3180 }
3181
3182 /// Substrate-canonical per-`Caixa` `:membros` M3 mesh-slot outer-
3183 /// composite MESH-COMPOSITION-shaped per-Aplicacao member-list slice
3184 /// accessor every consumer of the top-level manifest's per-Aplicacao
3185 /// `&[crate::aplicacao::Membro]` slice-view keys off — returns the
3186 /// author-declared `:membros` typed `Vec<crate::aplicacao::Membro>`
3187 /// verbatim as a `&[crate::aplicacao::Membro]` slice-view over the
3188 /// same backing buffer the raw `self.membros.as_slice()` field access
3189 /// borrows from. Empty-slice-carrying (the "no members declared" arm
3190 /// every non-`Aplicacao`-kind `defcaixa` carries by `#[serde(default)]`
3191 /// and every partially-authored Aplicacao carries before the
3192 /// [`crate::AplicacaoError::MembrosEmpty`] gate fires; the returned
3193 /// `&[Membro]` degenerates to an empty slice on those arms without any
3194 /// silent `None` collapse).
3195 ///
3196 /// The outer `:membros` slot carries the M3 typed MESH-COMPOSITION
3197 /// per-Aplicacao member list — the load-bearing container of every
3198 /// per-member `{caixa, versao}` pair the caixa-mesh renderer's
3199 /// per-Aplicacao program-emission dispatch fans on at mesh-artifact
3200 /// materialization time (MESH-COMPOSITION §III.1 — the typed graph's
3201 /// vertex set the `:contratos` `:de`/`:para` edges resolve against and
3202 /// the `:entrada :para` external-gateway destination validates
3203 /// against; CAIXA-SDLC §II — the typed-M3 slot algebra the operator's
3204 /// per-Aplicacao fan-out dispatch fans on). Every per-member axis
3205 /// threads through a lifted per-entry accessor on the
3206 /// [`crate::aplicacao::Membro`] type: the
3207 /// [`crate::aplicacao::Membro::nome`] DNS-1123-label member-caixa-
3208 /// identity scalar accessor (4a32abf) and the peer
3209 /// [`crate::aplicacao::Membro::versao_requirement`] SemVer-2
3210 /// version-requirement scalar accessor (a40b0e3). Every downstream
3211 /// consumer of the mesh-graph path first passes through this outer
3212 /// accessor onto the slice and then dispatches per-member through
3213 /// the inner accessors — the two-level dispatch means every per-
3214 /// `:membros` reader now routes through a typed dispatch on the
3215 /// substrate primitive at both altitudes.
3216 ///
3217 /// Prior to this lift the `.membros` `Vec<Membro>` slot was accessed
3218 /// inline at three production sites across two files — the
3219 /// [`Self::declared_mesh_slots`] mesh-slot declared-slot
3220 /// enumerator's `!self.membros.is_empty()` presence probe
3221 /// (caixa-core/src/manifest.rs, which drives the
3222 /// `M3_AUTHOR_KEY_MEMBROS` kebab-case author-label push every
3223 /// [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-coherence
3224 /// gate reads), the [`Self::aplicacao_view`] per-Aplicacao typed-view
3225 /// composer's `self.membros.clone()` per-member fold-in path
3226 /// (caixa-core/src/manifest.rs, which materializes the typed
3227 /// [`crate::aplicacao::AplicacaoSpec`] view every
3228 /// [`crate::StandardLayout::verify`] Aplicacao-arm gate dispatches
3229 /// on), and the [`crate::StandardLayout::verify`] per-`:membros
3230 /// :caixa` self-membership refusal probe's `&caixa.membros`-borrowed
3231 /// [`crate::aplicacao::validate_no_self_membership`] input
3232 /// (caixa-core/src/layout.rs, which pins the "no member names the
3233 /// Aplicacao's own `:nome`" cross-slot coherence gate). A future
3234 /// extension of the outer `:membros` axis (a per-cluster
3235 /// `:membros-overrides` overlay the wasm-engine operator resolves at
3236 /// admission time so a cluster-specific member-set can tighten a
3237 /// caixa-declared list without re-authoring the `caixa.lisp`,
3238 /// promotion of the plain `Vec<Membro>` to a richer
3239 /// `{static, dynamic}` partition once runtime-resolved Aplicacao
3240 /// members land as a typed axis, per-member priority annotation once
3241 /// multi-strategy fan-out lands) would have had to be threaded
3242 /// through all three open-coded copies in lockstep or one consumer
3243 /// would silently disagree with the peers on which member slice a
3244 /// given Caixa resolves to — the enumerator's presence probe reading
3245 /// the raw slot while the peer view-composer's fold-in path read an
3246 /// operator-resolved slot would silently split the paired
3247 /// declared-slot enumerator and typed-view composition, and the
3248 /// [`crate::aplicacao::validate_no_self_membership`] self-membership
3249 /// refusal probe reading a third borrow would silently drift the
3250 /// cross-slot coherence gate's traversal input from the two peers, a
3251 /// three-consumer split at the enumerator, the view composer, and
3252 /// the self-membership gate far from the source `caixa.lisp` with no
3253 /// field naming the member-set-drift root cause. Lifting the
3254 /// resolution rule to a typed method on the substrate primitive
3255 /// means every downstream consumer of the caixa's per-`Caixa`
3256 /// MESH-COMPOSITION outer-slice surface reaches for exactly one
3257 /// typed dispatch — the resolver's accept-set migrates as a unit on
3258 /// any future axis addition.
3259 ///
3260 /// Third outer top-level [`Caixa`] `&[Composite]`-return slice
3261 /// accessor for M2 / M3 typed-slot vec-carry axes — opens the outer-
3262 /// `Caixa` M3 mesh-slot arm of the `&[Composite]` composite-slice
3263 /// sub-family the sibling M2 [`Self::upgrade_from`] (2a1f907) /
3264 /// [`Self::children`] (c17b51e) accessors opened for the M2 vec-carry
3265 /// altitude. Peer at the outer altitude of the closed inner-
3266 /// [`crate::AplicacaoSpec::membros`] (6c77e36) accessor on the same
3267 /// MESH-COMPOSITION per-Aplicacao member-list axis — the two
3268 /// altitudes now share the same "byte-equal, borrow-shared" outer-
3269 /// accessor discipline. Sibling in shape to the peer outer-`Caixa`
3270 /// `&[Dep]`-return [`Self::deps`] (ad34b4e) / [`Self::deps_dev`]
3271 /// (f7fd81e) and `&[String]`-return [`Self::autores`] (b5d813f) /
3272 /// [`Self::etiquetas`] (78c7d3c) / [`Self::bibliotecas`] (8a36c23) /
3273 /// [`Self::exe`] (65d9527) / [`Self::servicos`] (611f78b) slice-
3274 /// accessors on the sibling outer-`Caixa` scalar-element vec-carry
3275 /// axes — folds the "outer [`Caixa`] `&[T]` slice" projection
3276 /// pattern onto the sibling M3 typed-composite-element axis
3277 /// ([`crate::aplicacao::Membro`] composite, matching the per-inner
3278 /// [`crate::AplicacaoSpec::membros`] element type at a different
3279 /// altitude).
3280 ///
3281 /// Returns `&[crate::aplicacao::Membro]` (not `&Vec<Membro>`)
3282 /// because every downstream consumer of the member list treats it
3283 /// as a read-only sequence — the slice-view is the narrowest borrow
3284 /// that supports every present + roadmapped consumer (`.iter()`,
3285 /// `.len()`, `.is_empty()`, the
3286 /// [`crate::aplicacao::validate_no_self_membership`] `&[Membro]`
3287 /// input, `serde` slice-serialization) without leaking the backing
3288 /// `Vec`'s grow/push/reserve surface no consumer of the typed view
3289 /// reaches for (the storage-side `Vec` remains reachable through the
3290 /// `pub membros` field for the mutation-carrying serde round-trip
3291 /// and per-test fixture-mutation paths, including the
3292 /// [`Self::aplicacao_view`] fold-in path that clones the slot into
3293 /// the typed view). Named `membros()` to match the storage field's
3294 /// name verbatim and the tatara-lisp author-surface term
3295 /// (`:membros`) the field's own docstring already carries; the
3296 /// accessor's identity maps onto the canonical MESH-COMPOSITION
3297 /// vocabulary the [`Caixa::membros`] field's docstring already
3298 /// reaches for ("Member Servicos that make up this Aplicacao").
3299 #[must_use]
3300 pub fn membros(&self) -> &[crate::aplicacao::Membro] {
3301 self.membros.as_slice()
3302 }
3303
3304 /// Substrate-canonical per-`Caixa` `:contratos` M3 mesh-slot outer-
3305 /// composite MESH-COMPOSITION-shaped per-Aplicacao WIT-typed
3306 /// inter-Servico contract-list slice accessor every consumer of the
3307 /// top-level manifest's per-Aplicacao `&[crate::aplicacao::WitContract]`
3308 /// slice-view keys off — returns the author-declared `:contratos`
3309 /// typed `Vec<crate::aplicacao::WitContract>` verbatim as a
3310 /// `&[crate::aplicacao::WitContract]` slice-view over the same
3311 /// backing buffer the raw `self.contratos.as_slice()` field access
3312 /// borrows from. Empty-slice-carrying (the "no contracts declared"
3313 /// arm every non-`Aplicacao`-kind `defcaixa` carries by
3314 /// `#[serde(default)]` and every leaf Aplicacao carrying only a
3315 /// single member with no inter-Servico edge carries; the returned
3316 /// `&[WitContract]` degenerates to an empty slice on those arms
3317 /// without any silent `None` collapse).
3318 ///
3319 /// The outer `:contratos` slot carries the M3 typed MESH-COMPOSITION
3320 /// per-Aplicacao WIT-typed inter-Servico edge list — the load-bearing
3321 /// container of every per-edge `{de, para, wit, endpoint | subject |
3322 /// slot}` quadruple the caixa-mesh renderer's per-Aplicacao
3323 /// `CiliumNetworkPolicy` fan-out (one L7 policy per edge —
3324 /// MESH-COMPOSITION §III.2 point 2) and per-`(:de, :para)`
3325 /// adjacency-list seed dispatch on at mesh-artifact materialization
3326 /// time (MESH-COMPOSITION §III.1 — the typed graph's edge set the
3327 /// `:membros` vertex set resolves against, closed by the
3328 /// [`crate::AplicacaoError::ContractoUnknownMember`] / cycle-refusal
3329 /// gates in §III.3; CAIXA-SDLC §II — the typed-M3 slot algebra the
3330 /// operator's per-Aplicacao fan-out dispatch fans on). Every
3331 /// per-edge axis threads through a lifted per-entry accessor on the
3332 /// [`crate::aplicacao::WitContract`] type: the peer `de` / `para`
3333 /// DNS-1123-label member-caixa-name endpoint scalar accessors, the
3334 /// [`crate::aplicacao::WitContract::endpoint`] (7020470) HTTP-shape
3335 /// / [`crate::aplicacao::WitContract::subject`] (90de675)
3336 /// NATS-pub-sub-shape / [`crate::aplicacao::WitContract::slot`]
3337 /// (ed22b66) `wasi:keyvalue/store`-shape payload-carrier accessors,
3338 /// and the WIT-world discriminant. Every downstream consumer of the
3339 /// mesh-graph edge path first passes through this outer accessor
3340 /// onto the slice and then dispatches per-contract through the
3341 /// inner accessors — the two-level dispatch means every
3342 /// per-`:contratos` reader now routes through a typed dispatch on
3343 /// the substrate primitive at both altitudes.
3344 ///
3345 /// Prior to this lift the `.contratos` `Vec<WitContract>` slot was
3346 /// accessed inline at two production sites in
3347 /// caixa-core/src/manifest.rs — the [`Self::declared_mesh_slots`]
3348 /// mesh-slot declared-slot enumerator's
3349 /// `!self.contratos.is_empty()` presence probe (which drives the
3350 /// `M3_AUTHOR_KEY_CONTRATOS` kebab-case author-label push every
3351 /// [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-coherence
3352 /// gate reads) and the [`Self::aplicacao_view`] per-Aplicacao
3353 /// typed-view composer's `self.contratos.clone()` per-contract
3354 /// fold-in path (which materializes the typed
3355 /// [`crate::aplicacao::AplicacaoSpec`] view every
3356 /// [`crate::StandardLayout::verify`] Aplicacao-arm gate and every
3357 /// downstream `caixa-mesh` renderer dispatches on). A future
3358 /// extension of the outer `:contratos` axis (a per-cluster
3359 /// `:contratos-overrides` overlay the wasm-engine operator resolves
3360 /// at admission time so a cluster-specific edge-set can tighten a
3361 /// caixa-declared list without re-authoring the `caixa.lisp`,
3362 /// promotion of the plain `Vec<WitContract>` to a richer
3363 /// `{static, dynamic}` partition once runtime-resolved contract
3364 /// edges land, per-edge policy annotation once the M4 per-edge
3365 /// policy overlay axis lands) would have had to be threaded through
3366 /// both open-coded copies in lockstep or one consumer would
3367 /// silently disagree with the peer on which edge slice a given
3368 /// Caixa resolves to — the enumerator's presence probe reading the
3369 /// raw slot while the peer view-composer's fold-in path read an
3370 /// operator-resolved slot would silently split the paired
3371 /// declared-slot enumerator and typed-view composition, a
3372 /// two-consumer split at the enumerator and the view composer far
3373 /// from the source `caixa.lisp` with no field naming the edge-set-
3374 /// drift root cause. Lifting the resolution rule to a typed method
3375 /// on the substrate primitive means every downstream consumer of
3376 /// the caixa's per-`Caixa` MESH-COMPOSITION outer-slice surface
3377 /// reaches for exactly one typed dispatch — the resolver's
3378 /// accept-set migrates as a unit on any future axis addition.
3379 ///
3380 /// Fourth and final outer top-level [`Caixa`] `&[Composite]`-return
3381 /// slice accessor for M2 / M3 typed-slot vec-carry axes — closes
3382 /// the outer-`Caixa` `&[Composite]` composite-slice sub-family the
3383 /// sibling M2 [`Self::upgrade_from`] (2a1f907) / [`Self::children`]
3384 /// (c17b51e) accessors opened and the M3 [`Self::membros`]
3385 /// (0f26987) accessor folded on, and closes the outer-`Caixa` M3
3386 /// mesh-slot arm of the composite-slice sub-family the sibling
3387 /// [`Self::membros`] accessor opened for the M3 vec-carry altitude.
3388 /// Peer at the outer altitude of the closed inner-
3389 /// [`crate::AplicacaoSpec::contratos`] (0dcc926) accessor on the
3390 /// same MESH-COMPOSITION per-Aplicacao contract-list axis — the two
3391 /// altitudes now share the same "byte-equal, borrow-shared" outer-
3392 /// accessor discipline. Sibling in shape to the peer outer-`Caixa`
3393 /// `&[Dep]`-return [`Self::deps`] (ad34b4e) / [`Self::deps_dev`]
3394 /// (f7fd81e) and `&[String]`-return [`Self::autores`] (b5d813f) /
3395 /// [`Self::etiquetas`] (78c7d3c) / [`Self::bibliotecas`] (8a36c23) /
3396 /// [`Self::exe`] (65d9527) / [`Self::servicos`] (611f78b) slice-
3397 /// accessors on the sibling outer-`Caixa` scalar-element vec-carry
3398 /// axes — folds the "outer [`Caixa`] `&[T]` slice" projection
3399 /// pattern onto the sibling M3 typed-composite-element axis
3400 /// ([`crate::aplicacao::WitContract`] composite, matching the
3401 /// per-inner [`crate::AplicacaoSpec::contratos`] element type at a
3402 /// different altitude).
3403 ///
3404 /// Returns `&[crate::aplicacao::WitContract]` (not
3405 /// `&Vec<WitContract>`) because every downstream consumer of the
3406 /// contract list treats it as a read-only sequence — the slice-view
3407 /// is the narrowest borrow that supports every present + roadmapped
3408 /// consumer (`.iter()`, `.len()`, `.is_empty()`, per-edge WIT-world
3409 /// discriminant dispatch, `serde` slice-serialization) without
3410 /// leaking the backing `Vec`'s grow/push/reserve surface no
3411 /// consumer of the typed view reaches for (the storage-side `Vec`
3412 /// remains reachable through the `pub contratos` field for the
3413 /// mutation-carrying serde round-trip and per-test fixture-mutation
3414 /// paths, including the [`Self::aplicacao_view`] fold-in path that
3415 /// clones the slot into the typed view). Named `contratos()` to
3416 /// match the storage field's name verbatim and the tatara-lisp
3417 /// author-surface term (`:contratos`) the field's own docstring
3418 /// already carries; the accessor's identity maps onto the canonical
3419 /// MESH-COMPOSITION vocabulary the [`Caixa::contratos`] field's
3420 /// docstring already reaches for ("WIT-typed inter-Servico
3421 /// contracts").
3422 #[must_use]
3423 pub fn contratos(&self) -> &[crate::aplicacao::WitContract] {
3424 self.contratos.as_slice()
3425 }
3426
3427 /// Compose the Aplicacao-related flat slots into a single typed
3428 /// [`crate::aplicacao::AplicacaoSpec`] for validation +
3429 /// downstream renderer consumption. Returns `None` when the
3430 /// caixa isn't a `:kind Aplicacao`.
3431 #[must_use]
3432 pub fn aplicacao_view(&self) -> Option<crate::aplicacao::AplicacaoSpec> {
3433 if !self.kind().is_aplicacao() {
3434 return None;
3435 }
3436 Some(crate::aplicacao::AplicacaoSpec {
3437 membros: self.membros().to_vec(),
3438 contratos: self.contratos().to_vec(),
3439 politicas: self.politicas().cloned().unwrap_or_default(),
3440 placement: self.placement().cloned().unwrap_or_default(),
3441 entrada: self.entrada().cloned(),
3442 })
3443 }
3444
3445 /// The kebab-case `:slot` tags of every M3 mesh slot this caixa
3446 /// *declares* a value on, in canonical declaration order
3447 /// (`:membros` → `:contratos` → `:politicas` → `:placement` →
3448 /// `:entrada`). A slot counts as declared when its backing field
3449 /// carries a value — a non-empty `Vec`, or a `Some(...)`.
3450 ///
3451 /// The M3 mesh slots compose the typed graph of a `:kind Aplicacao`
3452 /// (MESH-COMPOSITION §III.1). [`Self::aplicacao_view`] only folds
3453 /// them into a validatable [`crate::aplicacao::AplicacaoSpec`] when
3454 /// the kind matches (returns `None` otherwise), and the caixa-mesh /
3455 /// caixa-flux / caixa-helm renderers only emit them for an
3456 /// Aplicacao. On any *other* kind a declared mesh slot is the
3457 /// manifest field's documented "ignored otherwise" (see the
3458 /// `:membros` … `:entrada` field docs): it silently passes
3459 /// [`Caixa::from_lisp`] and then vanishes — never validated, never
3460 /// rendered — far from the source caixa.lisp.
3461 /// [`crate::StandardLayout::verify`] consults this to reject that
3462 /// silent-drop at caixa-build time
3463 /// ([`crate::LayoutError::MeshSlotsOnNonAplicacao`]), mirroring the
3464 /// `SupervisorOwnsCode` / `AplicacaoOwnsCode` kind-coherence gates:
3465 /// a slot foreign to the kind is a build error, not a silent drop.
3466 ///
3467 /// Lifted as a typed method (rather than an inline disjunction at
3468 /// the verify call site) so the mesh-slot set lives in one place —
3469 /// a future M4 axis added to the Aplicacao surface (per-edge policy
3470 /// overlay, distributed-app takeover config) is one push here, and
3471 /// every consumer reaching for "which mesh slots are set" (the
3472 /// verify gate, a future `feira lint` kind-coherence advisory)
3473 /// inherits the canonical order without rolling its own.
3474 ///
3475 /// Each per-arm kebab-case label is routed through the peer
3476 /// [`crate::M3_AUTHOR_KEY_MEMBROS`] /
3477 /// [`crate::M3_AUTHOR_KEY_CONTRATOS`] /
3478 /// [`crate::M3_AUTHOR_KEY_POLITICAS`] /
3479 /// [`crate::M3_AUTHOR_KEY_PLACEMENT`] /
3480 /// [`crate::M3_AUTHOR_KEY_ENTRADA`] consts declared next to the
3481 /// [`crate::M3_KEY_PLACEMENT`] renderer-side wire-key peer, so both
3482 /// halves of every M3 top-level mesh slot's dual axis (author-facing
3483 /// kebab-case label + renderer-side artifact key) route through one
3484 /// canonical declaration per arm — same discipline the peer
3485 /// [`crate::M2_AUTHOR_KEY_LIMITS`] / [`crate::M2_AUTHOR_KEY_BEHAVIOR`]
3486 /// / [`crate::M2_AUTHOR_KEY_UPGRADE_FROM`] top-level M2 slot consts
3487 /// (f49c8b0) establish on the sibling per-Servico M2 top-level slot
3488 /// axis, extended here to close the M3 mesh-slot author-facing-label
3489 /// axis so both altitudes of the typed-slot algebra
3490 /// (per-Servico M2 + per-Aplicacao M3) share the same
3491 /// "one canonical byte-string per arm, next to the axis" discipline.
3492 #[must_use]
3493 pub fn declared_mesh_slots(&self) -> Vec<&'static str> {
3494 let mut slots = Vec::new();
3495 if !self.membros().is_empty() {
3496 slots.push(crate::render::M3_AUTHOR_KEY_MEMBROS);
3497 }
3498 if !self.contratos().is_empty() {
3499 slots.push(crate::render::M3_AUTHOR_KEY_CONTRATOS);
3500 }
3501 if self.politicas().is_some() {
3502 slots.push(crate::render::M3_AUTHOR_KEY_POLITICAS);
3503 }
3504 if self.placement().is_some() {
3505 slots.push(crate::render::M3_AUTHOR_KEY_PLACEMENT);
3506 }
3507 if self.entrada().is_some() {
3508 slots.push(crate::render::M3_AUTHOR_KEY_ENTRADA);
3509 }
3510 slots
3511 }
3512
3513 /// The kebab-case `:slot` tags of every supervisor-tree slot this
3514 /// caixa *declares* a value on, in canonical declaration order
3515 /// (`:estrategia` → `:max-restarts` → `:restart-window` →
3516 /// `:children`). A slot counts as declared when its backing field
3517 /// carries a value — a `Some(...)`, or a non-empty `Vec`.
3518 ///
3519 /// The supervisor-tree slots compose the typed OTP supervisor of a
3520 /// `:kind Supervisor` (INSPIRATIONS §II.2; the `:estrategia` +
3521 /// `:children` field docs above). [`Self::supervisor_view`] only
3522 /// folds them into a validatable [`SupervisorSpec`] when the kind
3523 /// matches (returns `None` otherwise), and the wasm-operator's
3524 /// hierarchical reconciler only consumes them for a Supervisor. On
3525 /// any *other* kind a declared supervisor slot is the manifest
3526 /// field's documented "ignored otherwise" (see the `:estrategia` …
3527 /// `:children` field docs): it silently passes [`Caixa::from_lisp`]
3528 /// and then vanishes — never validated, never reconciled — far from
3529 /// the source caixa.lisp. [`crate::StandardLayout::verify`] consults
3530 /// this to reject that silent-drop at caixa-build time
3531 /// ([`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]), the
3532 /// exact mirror of the [`Self::declared_mesh_slots`] /
3533 /// [`crate::LayoutError::MeshSlotsOnNonAplicacao`] gate on the
3534 /// Aplicacao-only slot set: a slot foreign to the kind is a build
3535 /// error, not a silent drop.
3536 #[must_use]
3537 pub fn declared_supervisor_slots(&self) -> Vec<&'static str> {
3538 let mut slots = Vec::new();
3539 if self.estrategia().is_some() {
3540 slots.push(crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA);
3541 }
3542 if self.max_restarts().is_some() {
3543 slots.push(crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS);
3544 }
3545 if self.restart_window().is_some() {
3546 slots.push(crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW);
3547 }
3548 if !self.children().is_empty() {
3549 slots.push(crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN);
3550 }
3551 slots
3552 }
3553
3554 /// The kebab-case `:slot` tags of every M2 Servico-runtime slot this
3555 /// caixa *declares* a value on, in canonical declaration order
3556 /// (`:limits` → `:behavior` → `:upgrade-from`). A slot counts as
3557 /// declared when its backing field carries a value — a `Some(...)`,
3558 /// or a non-empty `Vec`.
3559 ///
3560 /// The M2 slots configure the runtime of a long-running wasm
3561 /// component, i.e. a `:kind Servico`: `:limits` is Lunatic
3562 /// per-process sandboxing (INSPIRATIONS §III.1), `:behavior` is the
3563 /// OTP `gen_server` callback set (§II.3), `:upgrade-from` is the OTP
3564 /// appup hot-code-reload table (§II.4). The caixa-helm / caixa-flux
3565 /// renderers gate on [`crate::require_kind`]`(_, Servico)` and only
3566 /// emit these slots for a Servico; on any *other* kind a declared M2
3567 /// slot is the manifest field's documented "ignored otherwise": its
3568 /// well-formedness is checked by [`crate::StandardLayout::verify`]
3569 /// but the value is never rendered into a chart / programs.yaml entry
3570 /// — it silently passes [`Caixa::from_lisp`] + `feira build` and then
3571 /// vanishes, far from the source caixa.lisp.
3572 /// [`crate::StandardLayout::verify`] consults this to reject that
3573 /// silent-drop at caixa-build time
3574 /// ([`crate::LayoutError::ServicoSlotsOnNonServico`]), the exact
3575 /// mirror of the [`Self::declared_mesh_slots`] /
3576 /// [`Self::declared_supervisor_slots`] gates on the peer
3577 /// kind-exclusive slot sets: a slot foreign to the kind is a build
3578 /// error, not a silent drop.
3579 ///
3580 /// Each per-arm kebab-case label is routed through the peer
3581 /// [`crate::M2_AUTHOR_KEY_LIMITS`] / [`crate::M2_AUTHOR_KEY_BEHAVIOR`] /
3582 /// [`crate::M2_AUTHOR_KEY_UPGRADE_FROM`] consts declared next to the
3583 /// [`crate::M2_KEY_LIMITS`] / [`crate::M2_KEY_BEHAVIOR`] /
3584 /// [`crate::M2_KEY_UPGRADE_FROM`] renderer-side wire-key peers, so
3585 /// both halves of the M2 top-level slot's dual axis (author-facing
3586 /// kebab-case label + renderer-side camelCase overlay-container wire
3587 /// key) route through one canonical declaration per arm — same
3588 /// discipline the peer [`crate::M2_BEHAVIOR_AUTHOR_KEY_ON_*`] sub-slot
3589 /// author-label consts (889dc18) establish on the sibling
3590 /// per-callback axis inside the `:behavior` overlay block.
3591 #[must_use]
3592 pub fn declared_servico_slots(&self) -> Vec<&'static str> {
3593 let mut slots = Vec::new();
3594 if self.limits().is_some() {
3595 slots.push(crate::render::M2_AUTHOR_KEY_LIMITS);
3596 }
3597 if self.behavior().is_some() {
3598 slots.push(crate::render::M2_AUTHOR_KEY_BEHAVIOR);
3599 }
3600 if !self.upgrade_from().is_empty() {
3601 slots.push(crate::render::M2_AUTHOR_KEY_UPGRADE_FROM);
3602 }
3603 slots
3604 }
3605
3606 /// The kebab-case `:slot` tags of every code-surface slot this caixa
3607 /// declares a value on that its [`CaixaKind`] doesn't natively own,
3608 /// in canonical declaration order (`:exe` → `:servicos`). A
3609 /// code-surface slot is owned by exactly one kind: `:exe` by
3610 /// [`CaixaKind::Binario`] (the nix-built executable surface), and
3611 /// `:servicos` by [`CaixaKind::Servico`] (the wasm component +
3612 /// `ComputeUnit` daemon surface).
3613 ///
3614 /// Each is silently ignored when declared on the wrong kind: the
3615 /// caixa-helm / caixa-flux / caixa-flake renderers gate on
3616 /// [`crate::require_kind`]`(_, <owning-kind>)`, so on any *other*
3617 /// code-running kind a declared `:exe` / `:servicos` is the manifest
3618 /// field's documented "ignored otherwise" — its path is checked for
3619 /// existence by the layout's `bibliotecas`/`exe`/`servicos` loops
3620 /// (which run after [`Caixa::from_lisp`]), but the value is never
3621 /// rendered into a build target or programs.yaml entry. It silently
3622 /// passes [`Caixa::from_lisp`] + `feira build`, far from the source
3623 /// caixa.lisp, with no field naming which slot is foreign.
3624 ///
3625 /// [`crate::StandardLayout::verify`] consults this to reject that
3626 /// silent-drop at caixa-build time
3627 /// ([`crate::LayoutError::ForeignCodeSlot`]), beside the M2
3628 /// servico-runtime, supervisor-tree, and M3 mesh kind-coherence
3629 /// gates ([`Self::declared_servico_slots`] /
3630 /// [`Self::declared_supervisor_slots`] /
3631 /// [`Self::declared_mesh_slots`]): the fourth kind ↔ slot algebra
3632 /// axis to be closed on the typed surface. The Supervisor /
3633 /// Aplicacao "no code at all" cases ([`crate::LayoutError::SupervisorOwnsCode`]
3634 /// / [`crate::LayoutError::AplicacaoOwnsCode`]) keep their dedicated
3635 /// diagnostics — they fire ahead of this gate on the same `verify`
3636 /// pass, so for Supervisor / Aplicacao the `OwnCode` arm always wins
3637 /// and this method is moot. For Biblioteca / Binario / Servico, this
3638 /// gate fires when a code-running kind declares another code-running
3639 /// kind's exclusive code surface.
3640 ///
3641 /// `:bibliotecas` is deliberately excluded — a Binario or Servico
3642 /// may legitimately ship a `lib/` helper that the underlying
3643 /// substrate (the nix flake for Binario, the wasm component build
3644 /// for Servico) bundles into its build, so the slot's
3645 /// declared-on-wrong-kind cardinality isn't a structural error on
3646 /// either code-running kind. A Biblioteca declaring `:bibliotecas`
3647 /// is the native case (the slot's owning kind). Supervisor /
3648 /// Aplicacao declaring `:bibliotecas` is gated upstream by
3649 /// [`crate::LayoutError::SupervisorOwnsCode`] /
3650 /// [`crate::LayoutError::AplicacaoOwnsCode`].
3651 ///
3652 /// Lifted as a typed method (rather than an inline disjunction at
3653 /// the verify call site) so the foreign-code-slot set lives in one
3654 /// place — a future kind that gains its own code-surface slot is
3655 /// one push here, and every consumer reaching for "which code
3656 /// surfaces are foreign to this kind" (the verify gate, a future
3657 /// `feira lint` kind-coherence advisory, the future `app-operator`'s
3658 /// per-caixa build-target classifier) inherits the canonical order
3659 /// without rolling its own.
3660 #[must_use]
3661 pub fn declared_foreign_code_slots(&self) -> Vec<&'static str> {
3662 let mut slots = Vec::new();
3663 if !self.exe().is_empty() && !self.kind().requires_exe() {
3664 slots.push(":exe");
3665 }
3666 if !self.servicos().is_empty() && !self.kind().requires_servicos() {
3667 slots.push(":servicos");
3668 }
3669 slots
3670 }
3671
3672 /// Validate every entry of `:deps` and `:deps-dev` through
3673 /// [`Dep::validate`] — closing the parity loop with the per-axis
3674 /// `:versao` gates already wired into the typed-graph
3675 /// ([`crate::AplicacaoSpec::validate_membros`] for `:membros`,
3676 /// 9888b13) and typed supervisor tree
3677 /// ([`crate::SupervisorSpec::validate`] for `:children`, b38ff3a).
3678 ///
3679 /// Until this gate landed `:deps :versao` and `:deps-dev :versao`
3680 /// were the only `:versao` axes still untyped past
3681 /// [`Caixa::from_lisp`]: the derive macro stored the requirement
3682 /// as a String without parsing it, so a malformed-but-non-empty
3683 /// requirement (`"^bad-version"`, `"^^0.1"`, `"v0.1"`, `"not-a-req"`)
3684 /// silently passed parse and the `semver::Error` surfaced at
3685 /// lacre-resolve time, far from the source caixa.lisp, with no
3686 /// field naming which `:deps` entry carried the typo. Lifting the
3687 /// gate here makes the four `:versao` typed surfaces (`:deps`,
3688 /// `:deps-dev`, `:membros`, `:children`) structurally equivalent —
3689 /// every requirement string past `validate_deps` is round-trippable
3690 /// through [`crate::parse_requirement`] without re-checking at the
3691 /// resolver layer.
3692 ///
3693 /// Both lists run through the same per-entry validator so a typo
3694 /// in `:deps-dev` surfaces with the same diagnostic as one in
3695 /// `:deps` — neither axis is a second-class citizen of the typed
3696 /// surface.
3697 ///
3698 /// Within each list, [`DepError::DuplicateNome`] closes the
3699 /// set-not-multiset discipline on the `:nome` axis: two entries
3700 /// naming the same caixa carry two `:versao` / `:fonte` / feature
3701 /// triples that the caixa-resolver's lacre pipeline collapses to one
3702 /// via its `HashMap`-keyed-by-`:nome` consumption — the second entry
3703 /// silently overwrites the first at `concrete_versao`-resolve time
3704 /// (the same "second wins / one silently overwrites the other"
3705 /// shape the peer typed-graph duplicate gates already close on every
3706 /// other Vec-shaped authoring surface that keys by name). The
3707 /// duplicate check fires per-list and runs *after* each per-entry
3708 /// [`Dep::validate`] call so a malformed-and-duplicated entry
3709 /// surfaces its narrower per-entry diagnostic
3710 /// ([`DepError::NomeInvalid`], [`DepError::VersaoInvalid`],
3711 /// [`DepError::FonteRepoEmpty`], …) before the cross-entry duplicate
3712 /// diagnostic — the canonical "per-entry shape before cross-entry
3713 /// uniqueness" precedence the peer `:children :caixa`
3714 /// ([`crate::SupervisorSpec::validate`]), `:membros :caixa`
3715 /// ([`crate::AplicacaoSpec::validate_membros`]), `:contratos`
3716 /// ([`crate::AplicacaoSpec::validate`]), `:placement :clusters`
3717 /// ([`crate::AplicacaoSpec::validate_placement`]),
3718 /// `:entrada :paths` ([`crate::AplicacaoSpec::validate`]),
3719 /// `:upgrade-from :from` ([`crate::upgrade::validate_upgrade_from`]),
3720 /// and the within-`:upgrade-from`-entry per-instruction-class
3721 /// singularity gates ([`crate::UpgradeError::DuplicateLoadModule`],
3722 /// [`crate::UpgradeError::DuplicateStateChange`],
3723 /// [`crate::UpgradeError::DuplicateCleanup`]) all establish.
3724 ///
3725 /// Cross-list (`:deps` ↔ `:deps-dev`) coincidence is *not* gated
3726 /// here: Cargo's `[dependencies]` + `[dev-dependencies]` accept the
3727 /// same name in both tables (the dev table's pin overrides the
3728 /// runtime table's pin in test/dev contexts), and caixa's surface
3729 /// mirrors that convention until a deliberate choice retires the
3730 /// override pattern. Only within-list duplicates are structurally
3731 /// incoherent — those are what this gate closes.
3732 pub fn validate_deps(&self) -> Result<(), DepError> {
3733 for &list in crate::dep::DepList::ALL {
3734 let mut seen = std::collections::HashSet::new();
3735 for dep in self.deps_of(list) {
3736 dep.validate()?;
3737 crate::render::insert_first_seen(&mut seen, dep.nome(), || {
3738 DepError::DuplicateNome {
3739 nome: dep.nome().to_string(),
3740 list: list.as_str(),
3741 }
3742 })?;
3743 }
3744 }
3745 Ok(())
3746 }
3747
3748 /// Reject `:nome` values the K8s apiserver would refuse at admission
3749 /// time. The top-level Caixa identity flows directly into every
3750 /// substrate-side artifact's `metadata.name` axis: the
3751 /// `lareira-<nome>` Helm chart name ([`caixa-helm::lib::chart_name`]),
3752 /// the programs.yaml `name:` entry the `lareira-fleet-programs`
3753 /// aggregator keys ComputeUnit derivation off
3754 /// ([`caixa-flux::lib::programs_yaml_entry`]), the
3755 /// `LABEL_APLICACAO` label value carried on every Aplicacao-owned
3756 /// pod and the per-`:contratos` CiliumNetworkPolicy `metadata.name`
3757 /// (`<aplicacao>-<de>-to-<para>`) and the per-`:entrada`
3758 /// `<aplicacao>-<para>` HTTPRoute `metadata.name`
3759 /// ([`caixa-mesh::lib::cilium_network_policies`],
3760 /// [`caixa-mesh::lib::gateway_routes`]), and the default
3761 /// `lib/<nome>.lisp` / `exe/<nome>` layout paths
3762 /// ([`crate::StandardLayout::verify`]). Each K8s apiserver-side
3763 /// schema enforces the DNS-1123 label rule on admission; a
3764 /// structurally invalid `:nome` (`"MyApp"` — the canonical
3765 /// "I copied the display name verbatim" footgun, `"my_app"` — the
3766 /// Python-/Postgres-leak, `"team.app"` — `:nome` is a single label
3767 /// not a subdomain, `"-app"` / `"app-"` — DNS-1123 boundary
3768 /// violations, `"my app"` — the paste-from-doc footgun, `"café"` —
3769 /// IDN must be pre-encoded as Punycode, the 64-byte UUID-shaped
3770 /// over-cap slug) silently passed [`Caixa::from_lisp`] and the
3771 /// failure surfaced at `kubectl apply` time as a `metadata.name:
3772 /// Invalid value` rejection on whichever derived artifact admitted
3773 /// first, far from the source `caixa.lisp` and without any field
3774 /// naming the offending `:nome`.
3775 ///
3776 /// Thin wrapper around [`crate::render::is_dns_1123_label`] (the
3777 /// substrate-side predicate the per-axis name gates already share:
3778 /// `:membros :caixa` 3f9d7a0, `:placement :clusters` 6cbb900,
3779 /// `:children :caixa` 31bfa43) that maps the shared parser-shaped
3780 /// reason into the [`ManifestError::NomeInvalid`] variant, so the
3781 /// diagnostic is self-locating (the offending `:nome` is named
3782 /// verbatim) and the author can grep their `caixa.lisp` for
3783 /// `:nome "<value>"` and fix it in one edit. Same diagnostic shape
3784 /// every per-axis sibling gate already exposes
3785 /// ([`crate::AplicacaoError::MembroCaixaInvalid`],
3786 /// [`crate::AplicacaoError::PlacementClusterInvalid`],
3787 /// [`crate::SupervisorError::ChildCaixaInvalid`]).
3788 ///
3789 /// Empty `:nome` (which [`Caixa::from_lisp`] does not reject — the
3790 /// derive macro stores the raw String) is gated by the narrower
3791 /// [`ManifestError::NomeEmpty`] arm before the predicate is
3792 /// consulted, mirroring the empty-first cascade every per-axis
3793 /// name gate already uses (e.g. `MembroCaixaEmpty` before
3794 /// `MembroCaixaInvalid`, `EmptyChildName` before `ChildCaixaInvalid`).
3795 pub fn validate_nome(&self) -> Result<(), ManifestError> {
3796 // Routes through the shared
3797 // [`crate::render::require_valid_dns_1123_label`] gate the peer
3798 // name axes each land on so drift between the eight axes'
3799 // accepted DNS-1123-label sets is structurally impossible.
3800 let nome = self.nome();
3801 crate::render::require_valid_dns_1123_label(
3802 nome,
3803 || ManifestError::NomeEmpty,
3804 |reason| ManifestError::NomeInvalid {
3805 nome: nome.to_string(),
3806 reason,
3807 },
3808 )
3809 }
3810
3811 /// Reject `:nome` values whose joint length with the canonical
3812 /// [`crate::LAREIRA_CHART_NAME_PREFIX`] (`"lareira-"`) overflows
3813 /// the K8s DNS-1123 label cap [`crate::DNS_1123_LABEL_MAX_LEN`]
3814 /// (63 bytes). Every per-Servico / per-Aplicacao renderer the
3815 /// substrate carries materializes the caixa's `:nome` through the
3816 /// canonical [`crate::lareira_chart_name`] helper (f7320d7) into a
3817 /// `lareira-<nome>` artifact that lands as a K8s `metadata.name` /
3818 /// Helm chart name / `HelmRelease` `release_name`: `caixa-helm`'s
3819 /// `ChartDir.name` + `Chart.yaml::name`
3820 /// (caixa-helm/src/lib.rs:207), `caixa-flux`'s `cluster_bundle`
3821 /// `HelmRelease` `chart:` slot (caixa-flux/src/lib.rs:329),
3822 /// `caixa-tatara`'s `process_for_aplicacao` `release_name` +
3823 /// `oci://<registry>/lareira-<nome>` chart ref
3824 /// (caixa-tatara/src/lib.rs:124,178). Helm's own `Chart.yaml::name`
3825 /// admission rule strict-parses against DNS-1123-label, the Helm
3826 /// operator's tracking-secret name is derived from `release_name`
3827 /// and is itself DNS-1123-label-bounded, and the rendered chart's
3828 /// K8s object `metadata.name` axes embed the chart name as a
3829 /// prefix — every one fails admission on a > 63-byte chart name.
3830 ///
3831 /// The per-axis [`Self::validate_nome`] gate (6c992f8) already
3832 /// caps `:nome` itself at 63 bytes via [`is_dns_1123_label`], so a
3833 /// `:nome` of 56–63 bytes silently passed validate (the inner
3834 /// DNS-1123 check accepts the bare `:nome`) but produced a
3835 /// `lareira-<nome>` of 64–71 bytes that the apiserver / `helm lint`
3836 /// rejected at admission — far from the source `caixa.lisp`, with
3837 /// no field naming the overflow root cause. The
3838 /// [`lareira_chart_name`] helper's own doc comment
3839 /// (caixa-core/src/render.rs:3198) explicitly deferred the fix:
3840 /// "the M4 admission webhook will pin the joint-length invariant
3841 /// when it lands". This gate lands the invariant at the
3842 /// manifest-validate layer rather than waiting for the apiserver
3843 /// — the same fail-at-the-source posture every peer per-axis
3844 /// value-shape gate (DNS-1123 on `:nome`, SemVer-2 on `:versao`,
3845 /// SPDX-expression-shape on `:licenca`, 4-digit decimal year on
3846 /// `:edicao`, etc.) takes.
3847 ///
3848 /// Thin wrapper around
3849 /// [`crate::render::is_lareira_chart_name_shape`] (the
3850 /// substrate-side predicate that composes [`lareira_chart_name`] +
3851 /// [`is_dns_1123_label`] via the lifted
3852 /// [`crate::LAREIRA_CHART_NAME_NOME_MAX_LEN`] budget); maps the
3853 /// shared parser-shaped reason into the
3854 /// [`ManifestError::NomeChartNameBudgetExceeded`] variant so the
3855 /// diagnostic is self-locating (the offending `:nome` is named
3856 /// verbatim alongside the rendered chart name and the budget) and
3857 /// the author can shorten in one edit. The gate runs across every
3858 /// `:kind` — `:nome` is the substrate-wide identity axis any
3859 /// future renderer the substrate adds can derive a
3860 /// `lareira-<nome>` artifact from, and uniform enforcement closes
3861 /// the drift footgun where a future kind grows a chart-emitting
3862 /// render path while the validate cascade doesn't catch it.
3863 ///
3864 /// Runs *after* [`Self::validate_nome`] so the narrower
3865 /// `NomeEmpty` / `NomeInvalid` shape diagnostics fire first — a
3866 /// structurally-malformed `:nome` (empty, uppercase, underscore,
3867 /// dot, leading/trailing hyphen, Unicode, > 63 bytes) surfaces its
3868 /// specific shape error rather than the chart-name-budget error,
3869 /// preserving the legitimate "well-shaped `:nome` that happens to
3870 /// overflow the joint cap" arm for this gate.
3871 pub fn validate_nome_chart_name_budget(&self) -> Result<(), ManifestError> {
3872 let nome = self.nome();
3873 crate::render::is_lareira_chart_name_shape(nome).map_err(|reason| {
3874 ManifestError::NomeChartNameBudgetExceeded {
3875 nome: nome.to_string(),
3876 reason,
3877 }
3878 })
3879 }
3880
3881 /// Reject `:versao` values that don't parse as [`semver::Version`].
3882 /// The top-level Caixa version flows directly into every
3883 /// substrate-side artifact that carries a "this is which version of
3884 /// the caixa" axis: the `lareira-<nome>` Helm chart's `Chart.yaml`
3885 /// `version:` + `appVersion:` axes ([`caixa-helm::lib`] —
3886 /// SemVer-2-strict at `helm template` / `helm install` time per
3887 /// https://helm.sh/docs/topics/charts/#charts-and-versioning), the
3888 /// `feira publish` Zig-style `v<versao>` git tag
3889 /// ([`caixa-flux::lib::programs_yaml_entry`] / the
3890 /// `caixa-publish.yml` reusable workflow), the programs.yaml entry's
3891 /// `versao:` value the `lareira-fleet-programs` aggregator carries
3892 /// onto each rendered ComputeUnit, the OCI image's `:v<versao>` /
3893 /// `:latest` tags the substrate's `wasi-service-flake` builds with
3894 /// `skopeo push`, the lacre closure's pinned versions
3895 /// ([`caixa-resolver`] keys `concrete_versao`), and the
3896 /// `:upgrade-from :from` references peers in this exact `versao`
3897 /// shape (`semver::Version`, not `VersionReq`). Each consumer
3898 /// expects a strict three-part `MAJOR.MINOR.PATCH` (optionally
3899 /// `-prerelease` and/or `+build`); a structurally invalid `:versao`
3900 /// (`"0.1"` — missing patch, the canonical "I shortened it" footgun;
3901 /// `"v0.1.0"` — the git-tag-shape-leaking-into-versao typo;
3902 /// `"latest"` / `"main"` — the "I confused it with a docker tag"
3903 /// footgun; `"^0.1"` / `"~0.1.2"` — the requirement-shape leaking
3904 /// into the version field a peer `:deps :versao` accepts;
3905 /// `"0.1.0.0"` — the four-part Java/Microsoft convention DNS
3906 /// SemVer-2 forbids) silently passed [`Caixa::from_lisp`] (the
3907 /// derive macro stores the raw String) and the failure surfaced at
3908 /// the *first* downstream consumer that strict-parses it: at
3909 /// `helm install` time as a chart-version rejection, at
3910 /// `feira publish` time as a malformed git tag, at lacre-resolve
3911 /// time as a `semver::Error` not naming the offending caixa, at
3912 /// `feira upgrade --to <versao>` time as an unresolvable
3913 /// `:upgrade-from :from` match — far from the source `caixa.lisp`
3914 /// and without any field naming the offending `:versao`.
3915 ///
3916 /// Thin wrapper around [`semver::Version::parse`] — the same parser
3917 /// [`crate::CaixaVersion::parse`] (the typed `:versao` accessor)
3918 /// and [`crate::UpgradeFromEntry::validate`] (the peer
3919 /// `:upgrade-from :from` axis, 26da2c7) consume. Maps the
3920 /// `semver::Error` reason into the [`ManifestError::VersaoInvalid`]
3921 /// variant, carrying the offending `:versao` verbatim + a
3922 /// parser-shaped reason naming the specific violation, so the
3923 /// diagnostic is self-locating (the author can grep their
3924 /// `caixa.lisp` for `:versao "<value>"` and fix it in one edit).
3925 /// Same diagnostic shape as [`ManifestError::NomeInvalid`]
3926 /// (6c992f8) and [`crate::UpgradeError::FromInvalid`]
3927 /// (b0c8389) on the peer axes. With this gate, the typed `:versao`
3928 /// surfaces — top-level `:versao`, `:upgrade-from :from` — are
3929 /// now structurally equivalent (every value past validate is
3930 /// round-trippable through [`semver::Version::parse`] without
3931 /// re-checking at the renderer, resolver, or operator hot-upgrade
3932 /// layer), peer with the four `:versao` requirement axes (`:deps`,
3933 /// `:deps-dev`, `:membros`, `:children`) the prior commits
3934 /// (2420c44, 9888b13, b38ff3a) wired through `parse_requirement`.
3935 ///
3936 /// Empty `:versao` (which [`Caixa::from_lisp`] does not reject —
3937 /// the derive macro stores the raw String) is gated by the
3938 /// narrower [`ManifestError::VersaoEmpty`] arm before the parser is
3939 /// consulted, mirroring the empty-first cascade every per-axis
3940 /// version gate already uses (e.g. `MembroVersaoEmpty` before
3941 /// `MembroVersaoInvalid`, `EmptyChildVersion` before
3942 /// `ChildVersaoInvalid`, `NomeEmpty` before `NomeInvalid`).
3943 pub fn validate_versao(&self) -> Result<(), ManifestError> {
3944 let versao = self.versao();
3945 if versao.is_empty() {
3946 return Err(ManifestError::VersaoEmpty);
3947 }
3948 semver::Version::parse(versao).map_err(|e| ManifestError::VersaoInvalid {
3949 versao: versao.to_string(),
3950 reason: e.to_string(),
3951 })?;
3952 Ok(())
3953 }
3954
3955 /// Reject `:restart-window` values the shared
3956 /// [`crate::supervisor::duration_codec::parse`] refuses. The flat
3957 /// `restart_window: Option<String>` slot on [`Caixa`] is stored
3958 /// raw by the derive macro (the typed [`SupervisorSpec`] holds an
3959 /// `Option<Duration>` routed through the shared codec via `with =
3960 /// "duration_codec"`); the inline `Caixa → SupervisorSpec`
3961 /// view-construction path ([`Self::supervisor_view`]) folds the
3962 /// raw string through the same shared codec and soft-swallows the
3963 /// parse error as `None` to keep the view best-effort. Without
3964 /// this gate a malformed `:restart-window` (`"1.5s"` — the
3965 /// fractional-seconds drift class; `"1.0s"` — the decimal-shaped
3966 /// integer drift; `"0.5m"` — the unit-fraction drift; `"+30s"` /
3967 /// `"-30s"` — the leading-sign drift; `"30x"` — the unknown-unit
3968 /// footgun; `"abc"` — pure garbage; `""` — the empty-after-trim
3969 /// edge case) silently produced a `SupervisorSpec` with
3970 /// `restart_window: None`, indistinguishable from the canonical
3971 /// "omit the slot to express no reset" authoring shape — Erlang/OTP's
3972 /// `MaxIntensity / Period` invariant turns into a never-reset
3973 /// supervisor far from the source `caixa.lisp`, with no field
3974 /// naming the offending `:restart-window`. Lifting the gate to a
3975 /// Caixa-level validator mirrors the trajectory of the peer
3976 /// per-axis identity gates ([`Self::validate_nome`] 6c992f8,
3977 /// [`Self::validate_versao`] 1fdaa02, [`Self::validate_deps`]
3978 /// a7f0d8c) and the ABSORPTION-ROADMAP.md M2.2 test pin
3979 /// (line 196: "reject invalid `:restart-window` (non-duration)").
3980 ///
3981 /// Thin wrapper around [`crate::supervisor::duration_codec::parse`]
3982 /// (the shared codec backing `:supervisor :restart-window` as
3983 /// serde-routed on [`SupervisorSpec`], `:politicas :timeout`, and
3984 /// `:politicas :circuit-breaker :window` — all three covered by
3985 /// the integer-magnitude gate 1c55a2a). Maps the codec's parse
3986 /// error verbatim into the [`ManifestError::RestartWindowMalformed`]
3987 /// variant, carrying the offending raw string + a parser-shaped
3988 /// reason naming the canonical authoring form, so the diagnostic
3989 /// is self-locating (the author can grep their `caixa.lisp` for
3990 /// `:restart-window "<value>"` and fix it in one edit) and
3991 /// uniform with every other manifest-level validate diagnostic.
3992 /// With this gate the four `:restart-window`-shaped surfaces (the
3993 /// flat raw string on [`Caixa`], the typed `Option<Duration>` on
3994 /// [`SupervisorSpec`], the two `MeshPolicy` peer durations) are
3995 /// now structurally equivalent — every value past the codec is in
3996 /// one accepted set, by construction.
3997 ///
3998 /// `None` (the canonical "omit the slot to express no reset"
3999 /// shape) is accepted trivially — the gate is a no-op when the
4000 /// author didn't author a window. The empty string is rejected by
4001 /// the shared codec (its digit-only gate refuses an empty
4002 /// magnitude), surfacing the same `RestartWindowMalformed`
4003 /// diagnostic as every other rejected non-canonical shape.
4004 pub fn validate_restart_window(&self) -> Result<(), ManifestError> {
4005 let Some(s) = self.restart_window() else {
4006 return Ok(());
4007 };
4008 crate::supervisor::duration_codec::parse(s)
4009 .map(|_| ())
4010 .map_err(|reason| ManifestError::RestartWindowMalformed {
4011 restart_window: s.to_string(),
4012 reason,
4013 })
4014 }
4015
4016 /// Reject per-entry values on the three Caixa-level code-surface
4017 /// path lists (`:bibliotecas`, `:exe`, `:servicos`) that the
4018 /// layout checker's `root.join(p)` sandbox would silently subvert.
4019 /// Same three structural footguns the peer
4020 /// [`BehaviorSpec::validate`] (b0c8389) and
4021 /// [`crate::UpgradeInstruction::validate`] `StateChange` arm
4022 /// (26da2c7) already close on the M2 `:behavior :on-*` and
4023 /// `:upgrade-from :state-change :script` axes, here lifted onto
4024 /// the three top-level code-path axes through the shared
4025 /// [`is_sandboxed_relative_path`] predicate:
4026 ///
4027 /// - empty entry (`(:bibliotecas (""))` / `(:exe (""))` /
4028 /// `(:servicos (""))`): `PathBuf::new()` round-trips through
4029 /// [`Path::join`] as the base itself — `root.join("")` ==
4030 /// `root`, so the existence check (`self.exists(&root)`)
4031 /// trivially passes (the project root exists), and the layout
4032 /// silently treats the project root as a biblioteca / exe /
4033 /// servico entry. The `:bibliotecas` loop then hands the root
4034 /// to `tatara_lisp::read` at `feira build` time as if the root
4035 /// directory itself were a Lisp source file — a parse error
4036 /// far from the source `caixa.lisp` with no field naming the
4037 /// offending entry.
4038 /// - absolute path (`(:bibliotecas ("/etc/passwd"))`):
4039 /// [`Path::join`] *replaces* the base when the right-hand side
4040 /// is absolute, so `root.join("/etc/passwd")` resolves to
4041 /// `"/etc/passwd"` and escapes the project sandbox entirely.
4042 /// The existence check then silently consults whatever the
4043 /// escaped path resolves to — for `:bibliotecas`, the layout
4044 /// has no `starts_with`-fence (only `:exe` is fenced under
4045 /// `exe/` and `:servicos` under `servicos/`), so an absolute
4046 /// `:bibliotecas` entry that happens to resolve on disk
4047 /// silently passes. For `:exe` / `:servicos` the fence catches
4048 /// the absolute case downstream as `ExeOutsideDir` /
4049 /// `ServicoOutsideDir` (or `MissingEntry` if the absolute path
4050 /// doesn't exist), but with a downstream-shaped diagnostic
4051 /// that names the resolved escape path rather than the
4052 /// authoring footgun at the source.
4053 /// - parent-escape (`(:bibliotecas ("../sibling/x.lisp"))` /
4054 /// `(:exe ("exe/../../escape.lisp"))`): a [`PathBuf`] with any
4055 /// [`std::path::Component::ParentDir`] anywhere round-trips
4056 /// through [`Path::join`] as a traversal above the caixa root.
4057 /// The `:exe` / `:servicos` `starts_with(<dir>)` fence is
4058 /// *component-aware* (not canonical-path-aware), so
4059 /// `root.join("exe/../../escape.lisp")` `starts_with(exe_dir)`
4060 /// is **true** even though the canonical resolution
4061 /// `{parent of root}/escape.lisp` lives outside the caixa root
4062 /// — the fence silently lets the parent-escape through, and
4063 /// the existence check passes if that escape-target happens
4064 /// to exist. Caught regardless of where the `..` sits
4065 /// (leading, mid-path, trailing) so the gate matches the peer
4066 /// predicate's full coverage.
4067 ///
4068 /// Same `Empty` → `Absolute` → `ParentEscape` arm-ordering every peer
4069 /// `is_sandboxed_relative_path` consumer follows (b0c8389 / 26da2c7);
4070 /// same per-slot diagnostic shape every peer per-axis path-gate
4071 /// exposes (`*Empty { slot }` / `*Absolute { slot, path }` /
4072 /// `*ParentEscape { slot, path }`). Cross-slot precedence is
4073 /// `:bibliotecas` → `:exe` → `:servicos` — the same declaration
4074 /// order [`Caixa::declared_foreign_code_slots`] uses for its
4075 /// canonical foreign-code-slot diagnostic, so a manifest with
4076 /// multiple malformed slots surfaces the lexicographically-earliest
4077 /// slot's diagnostic deterministically.
4078 ///
4079 /// Lifted to the typed surface as a Caixa-level validator (peer
4080 /// of [`Self::validate_nome`] / [`Self::validate_versao`] /
4081 /// [`Self::validate_deps`] / [`Self::validate_restart_window`])
4082 /// and wired into [`crate::StandardLayout::verify`] before the
4083 /// existence-check loops so the diagnostic names the offending
4084 /// slot at the source caixa.lisp rather than reporting a
4085 /// downstream `MissingEntry` / `ExeOutsideDir` /
4086 /// `ServicoOutsideDir` against the resolved sandbox-escape path.
4087 /// The fourth typed code-path surface — every author-supplied
4088 /// path on the manifest — is now structurally accept-shaped
4089 /// past validate, peer with `:behavior :on-*` and
4090 /// `:upgrade-from :state-change :script`.
4091 pub fn validate_code_paths(&self) -> Result<(), ManifestError> {
4092 /// Per-slot file-type contract for the three Caixa-level
4093 /// code-path surfaces (`:bibliotecas`, `:exe`, `:servicos`).
4094 /// Each variant names the predicate the per-entry file-type
4095 /// gate consults; [`Self::None`] opts the slot out of any
4096 /// file-type contract. Lifted as a typed local enum so the
4097 /// per-slot dispatch is exhaustive at the `match` — adding a
4098 /// future axis to the typed-substrate `:` slot set (the
4099 /// future `:assets` resource axis the M5 roadmap names, the
4100 /// future `:nix-flake` derivation axis the caixa-flake
4101 /// emitter consults) lands as one variant + one `match` arm,
4102 /// not a coordinated rewrite of every per-slot bool flag.
4103 ///
4104 /// Peer of the typed-substrate per-slot variant disciplines
4105 /// already established on this surface
4106 /// ([`crate::supervisor::RestartStrategy`] +
4107 /// [`crate::supervisor::RestartPolicy`] on the OTP-shape
4108 /// supervision-tree axis,
4109 /// [`crate::aplicacao::PlacementStrategy`] on the §III.1
4110 /// placement axis, [`crate::aplicacao::WitTarget`] on the
4111 /// `:contratos` payload-target axis): the typed `enum` is
4112 /// the substrate's single source of truth for the per-axis
4113 /// dispatch, and every consumer (the per-arm body here, the
4114 /// future feira-lint per-slot diagnostic renderer, the M4
4115 /// per-axis admission webhook) reaches for the same typed
4116 /// surface rather than re-deriving the partition from inline
4117 /// flag combinations.
4118 enum CodePathFileType {
4119 /// `:exe` — nix-build derivation output, no terminating-
4120 /// extension contract (the canonical `"exe/<name>"`
4121 /// fixtures the layout's `ExeOutsideDir` error message
4122 /// documents carry no extension by convention).
4123 None,
4124 /// `:bibliotecas` — tatara-lisp source files the
4125 /// `feira build` loop reads through `tatara_lisp::read`
4126 /// at parse time. Routes to [`is_lisp_extension`].
4127 LispSource,
4128 /// `:servicos` — ComputeUnit-CR YAML files the
4129 /// caixa-helm / caixa-flux renderers consume through
4130 /// `serde_yaml::from_str`. Routes to
4131 /// [`is_computeunit_yaml_extension`].
4132 ComputeUnitYaml,
4133 }
4134
4135 // The per-slot [`CodePathFileType`] selects which axes carry the
4136 // lifted file-type predicate. `:bibliotecas` is the tatara-lisp
4137 // source axis (the `feira build` loop at
4138 // `caixa-feira/src/cmd/build.rs:33` reads each entry through
4139 // `tatara_lisp::read` at parse time) — the lifted
4140 // [`is_lisp_extension`] predicate gates the `.lisp` extension.
4141 // `:exe` is the nix-built executable surface (per the canonical
4142 // `"exe/<name>"`-shaped fixtures the layout's `ExeOutsideDir`
4143 // error message documents and every in-tree
4144 // `caixa_with_code_paths` positive control uses) — its file-type
4145 // contract is "nix-build derivation output", not a typed source
4146 // file, so [`CodePathFileType::None`] opts the slot out of any
4147 // file-type gate. `:servicos` is the `.computeunit.yaml`
4148 // ComputeUnit-CR axis (the peer caixa-helm / caixa-flux
4149 // renderers consume each entry through `serde_yaml::from_str` as
4150 // a typed `ComputeUnit` CR) — the lifted
4151 // [`is_computeunit_yaml_extension`] predicate gates the compound
4152 // `.computeunit.yaml` suffix. All three axes are surfaced through
4153 // the same iteration so the sandbox-shape + duplicate gates
4154 // apply uniformly; the typed file-type dispatch fires per-slot
4155 // exactly where the downstream consumer's accepted set demands
4156 // it. The third file-type variant ([`ComputeUnitYaml`]) is the
4157 // compounding lift on the peer 64772a9 `:bibliotecas`
4158 // `.lisp`-gate trajectory — the second of the three code-path
4159 // axes to land on a typed compound-suffix gate, with the same
4160 // self-locating per-slot diagnostic shape every peer per-axis
4161 // file-type lift uses (`*NonLispExtension { slot, path }` /
4162 // `*NonComputeUnitYamlExtension { slot, path }`).
4163 for (slot, list, file_type) in [
4164 (
4165 ":bibliotecas",
4166 &self.bibliotecas,
4167 CodePathFileType::LispSource,
4168 ),
4169 (":exe", &self.exe, CodePathFileType::None),
4170 (
4171 ":servicos",
4172 &self.servicos,
4173 CodePathFileType::ComputeUnitYaml,
4174 ),
4175 ] {
4176 // Per-slot set-not-multiset gate on the typed code-path axis.
4177 // Every peer Vec-shaped author-supplied list past validate is
4178 // a set, not a multiset: `:membros :caixa`
4179 // ([`crate::AplicacaoError::MembroDuplicate`]), `:placement
4180 // :clusters` ([`crate::AplicacaoError::PlacementClusterDuplicate`]),
4181 // `:entrada :paths` ([`crate::AplicacaoError::EntradaPathDuplicate`]),
4182 // `:contratos` ([`crate::AplicacaoError::ContratoDuplicate`]),
4183 // `:children :caixa` ([`crate::SupervisorError::DuplicateChild`]),
4184 // `:deps` / `:deps-dev` `:nome` ([`crate::DepError::DuplicateNome`]
4185 // per 359fba5), `:upgrade-from :from` ([`crate::UpgradeError::DuplicateFrom`]),
4186 // `:etiquetas` ([`ManifestError::EtiquetaDuplicate`] per 360a499),
4187 // `:autores` ([`ManifestError::AutorDuplicate`] per 86c769b) —
4188 // the three code-path lists are the last Vec-shaped author-
4189 // supplied slots on the typed Caixa surface still admitting a
4190 // duplicate entry silently. Scope is per-list (`:bibliotecas`
4191 // duplicates are flagged within `:bibliotecas`, not across
4192 // `:bibliotecas` ↔ `:exe`) — the same per-list scope `:deps`
4193 // ↔ `:deps-dev` use (a `:nome` present in both lists is a
4194 // legitimate dev-vs-runtime shape on the dep axis, fenced
4195 // separately by [`crate::dep::validate_no_self_dep`]). On the
4196 // code-path axis a cross-slot collision is structurally
4197 // impossible by the layout's `starts_with(<exe|servicos>_dir)`
4198 // fence — `:exe` and `:servicos` entries are confined to their
4199 // own directory trees, so the only way a string could appear
4200 // on two code-path lists is the (rare, structurally invalid)
4201 // case where `:bibliotecas` carries an `"exe/<x>"` or
4202 // `"servicos/<x>.yaml"`-shaped path.
4203 //
4204 // Without the gate three authoring footguns silently passed:
4205 //
4206 // - `:bibliotecas ("lib/foo.lisp" "lib/foo.lisp")` — the
4207 // canonical copy-paste-the-wrong-file footgun. `feira
4208 // build` (`caixa-feira/src/cmd/build.rs:33`) walks the
4209 // list and re-parses the same file twice, wasting work
4210 // and silently masking the author's intent to declare a
4211 // *second* biblioteca.
4212 // - `:exe ("exe/cli" "exe/cli")` — the same footgun on the
4213 // Binario surface. The future `caixa-flake` `nix flake`
4214 // emitter that materializes each `:exe` entry as a flake
4215 // `packages.<exe-name>` derivation would collide on the
4216 // duplicate package name and surface a flake-eval error
4217 // far from the source `caixa.lisp`.
4218 // - `:servicos ("servicos/x.computeunit.yaml"
4219 // "servicos/x.computeunit.yaml")` — the same footgun on
4220 // the Servico surface. The peer `caixa-helm` / `caixa-flux`
4221 // renderers already refuse `:servicos.len() != 1` with
4222 // the narrower [`UnsupportedServicoCount`] diagnostic, but
4223 // that diagnostic surfaces "too many servicos" without
4224 // naming "duplicate entry" — the typed self-locating
4225 // "which entry is the duplicate" framing only lands at
4226 // this gate.
4227 //
4228 // Same `seen.insert(entry.as_str())` shape every peer per-list
4229 // duplicate gate uses (`:etiquetas` 360a499, `:autores`
4230 // 86c769b, `:deps` 359fba5) and the same "structural shape
4231 // checks fire before the duplicate check on the same entry"
4232 // ordering (a `(:bibliotecas ("" "lib/x.lisp" "lib/x.lisp"))`
4233 // shape surfaces the narrower [`Self::CodePathEmpty`] for the
4234 // empty entry first, not the duplicate on the later pair).
4235 let mut seen = std::collections::HashSet::new();
4236 for entry in list {
4237 let path = Path::new(entry);
4238 match is_sandboxed_relative_path(path) {
4239 Ok(()) => {}
4240 Err(PathShapeViolation::Empty) => {
4241 return Err(ManifestError::CodePathEmpty { slot });
4242 }
4243 Err(PathShapeViolation::Absolute) => {
4244 return Err(ManifestError::CodePathAbsolute {
4245 slot,
4246 path: path.to_path_buf(),
4247 });
4248 }
4249 Err(PathShapeViolation::ParentEscape) => {
4250 return Err(ManifestError::CodePathParentEscape {
4251 slot,
4252 path: path.to_path_buf(),
4253 });
4254 }
4255 }
4256 // The per-slot file-type gate dispatched through the
4257 // typed [`CodePathFileType`] selector above. Each variant
4258 // routes to the lifted predicate the downstream consumer
4259 // demands:
4260 //
4261 // - [`LispSource`] → [`is_lisp_extension`] for
4262 // `:bibliotecas` (the `feira build` loop's
4263 // `tatara_lisp::read` consumer);
4264 // - [`ComputeUnitYaml`] → [`is_computeunit_yaml_extension`]
4265 // for `:servicos` (the caixa-helm / caixa-flux
4266 // `serde_yaml::from_str` consumer's `ComputeUnit` CR
4267 // accepted set);
4268 // - [`None`] for `:exe` — the nix-build derivation-
4269 // output axis has no terminating-extension contract.
4270 //
4271 // Fires after the sandbox-shape arms so a path that is
4272 // *both* sandbox-escaping and wrong-extension surfaces
4273 // the more fundamental sandbox-shape diagnostic first
4274 // (mirrors the peer `EmptyPath` → `AbsolutePath` →
4275 // `ParentEscape` → `NonLispExtension` arm-ordering on
4276 // `:behavior :on-*` c97815a, and `EmptyScript` →
4277 // `AbsoluteScript` → `ParentEscapeScript` →
4278 // `NonLispExtensionScript` on
4279 // `:upgrade-from :state-change :script` 33cc830), and
4280 // before the duplicate gate so the narrower per-entry
4281 // file-type shape dominates the cross-entry uniqueness
4282 // diagnostic (a
4283 // `("servicos/x.yaml" "servicos/x.yaml")` shape on
4284 // `:servicos` surfaces
4285 // `CodePathNonComputeUnitYamlExtension` on the first
4286 // entry rather than `CodePathDuplicate` on the pair —
4287 // peer with the 64772a9 `:bibliotecas`
4288 // `("lib/x.txt" "lib/x.txt")` ordering).
4289 match file_type {
4290 CodePathFileType::None => {}
4291 CodePathFileType::LispSource => {
4292 if !is_lisp_extension(path) {
4293 return Err(ManifestError::CodePathNonLispExtension {
4294 slot,
4295 path: path.to_path_buf(),
4296 });
4297 }
4298 }
4299 CodePathFileType::ComputeUnitYaml => {
4300 if !is_computeunit_yaml_extension(path) {
4301 return Err(ManifestError::CodePathNonComputeUnitYamlExtension {
4302 slot,
4303 path: path.to_path_buf(),
4304 });
4305 }
4306 }
4307 }
4308 crate::render::insert_first_seen(&mut seen, entry.as_str(), || {
4309 ManifestError::CodePathDuplicate {
4310 slot,
4311 path: path.to_path_buf(),
4312 }
4313 })?;
4314 }
4315 }
4316 Ok(())
4317 }
4318
4319 /// Reject `:etiquetas` lists with an empty entry or with two entries
4320 /// agreeing on the same string. `:etiquetas` is the universal
4321 /// registry-search-tag axis on [`Caixa`] (every kind carries the
4322 /// `Vec<String>` slot) and lands verbatim as the Helm chart
4323 /// `Chart.yaml` `keywords:` array on every Servico (caixa-helm's
4324 /// `build_chart_yaml` at `caixa-helm/src/lib.rs:236` folds it through
4325 /// a [`std::collections::BTreeSet`] alongside the four substrate-
4326 /// fixed tags `lareira` / `wasm` / `tatara-lisp` / `caixa-servico`).
4327 /// Two authoring footguns silently passed validate without this gate:
4328 ///
4329 /// - Empty entry (`(:etiquetas (""))` — the canonical paste-from-
4330 /// blank-doc footgun) rendered as `keywords: ["", "caixa-servico",
4331 /// "lareira", "tatara-lisp", "wasm"]` in `Chart.yaml`. Helm's
4332 /// `chart.metadata.keywords` admits the value without a strict
4333 /// parser-side gate, but the empty keyword has no operational
4334 /// meaning — it indexes nothing in the future caixa-registry
4335 /// search axis and clutters the rendered chart with a no-op tag.
4336 /// - Duplicate entries (`(:etiquetas ("demo" "demo"))` — the
4337 /// copy-paste-the-wrong-tag footgun) silently passed validate
4338 /// and were silently dedup'd by caixa-helm's `BTreeSet` collect
4339 /// at chart render — a "second wins / one silently disappears"
4340 /// shape divergent from every peer typed-graph set gate
4341 /// ([`crate::AplicacaoError::MembroDuplicate`] on `:membros`,
4342 /// [`crate::AplicacaoError::PlacementClusterDuplicate`] on
4343 /// `:placement :clusters`, [`crate::AplicacaoError::EntradaPathDuplicate`]
4344 /// on `:entrada :paths`, [`crate::AplicacaoError::ContratoDuplicate`]
4345 /// on `:contratos`, [`crate::DepError::DuplicateNome`] on
4346 /// `:deps` / `:deps-dev` per 359fba5, [`crate::UpgradeError::DuplicateFrom`]
4347 /// on `:upgrade-from`, the per-instruction-class singularity
4348 /// gates [`crate::UpgradeError::DuplicateLoadModule`] /
4349 /// [`crate::UpgradeError::DuplicateStateChange`] /
4350 /// [`crate::UpgradeError::DuplicateCleanup`]). The typed-graph
4351 /// discipline is uniform: every Vec-shaped author-supplied list
4352 /// past validate is set-not-multiset, by construction.
4353 ///
4354 /// Past the empty arm the gate enforces the chart-keyword shape
4355 /// predicate via [`crate::render::is_chart_keyword_shape`]: Cargo's
4356 /// crates.io `[package] keywords` grammar — 1..=20 bytes, starts
4357 /// with an ASCII letter, ASCII alphanumeric / `_` / `-`
4358 /// continuation. Closes the canonical paste-from-doc footguns the
4359 /// bare empty + duplicate arms left open: paste-from-aligned-doc
4360 /// whitespace (`" mesh"`, `"mesh "`), paste-from-multiline-doc
4361 /// newline (`"mesh\nhttp"` — the author pasted a multi-tag block
4362 /// into one entry instead of splitting), paste-from-Windows-CRLF-doc
4363 /// carriage return, CSV-list-separator confusion (`"mesh,http,grpc"`
4364 /// — the author meant three separate list entries), path-separator
4365 /// confusion (`"caixa/servico"`), namespace-suffix (`"http.1"`),
4366 /// leading-digit (`"1foo"`), kebab-leak (`"-foo"`), snake-leak
4367 /// (`"_foo"`), non-ASCII (`"café"`), and paste-from-binary-blob
4368 /// control bytes that would silently land as malformed search tags
4369 /// in the rendered Chart.yaml `keywords:` array and break the
4370 /// Artifact Hub keyword index lookup far from the source caixa.lisp.
4371 /// Mirrors the [`Self::validate_autores`] shape-predicate cascade
4372 /// established on the sibling universal-axis `Vec<String>` surface
4373 /// — the second universal-axis Vec<String> surface to land the
4374 /// empty-first-then-shape-then-duplicate per-entry cascade.
4375 ///
4376 /// Same empty-first cascade discipline every peer per-axis gate
4377 /// uses: the per-entry empty arm fires before the per-entry shape
4378 /// arm fires before the cross-entry duplicate arm, so an
4379 /// `("" "mesh" "mesh")` authoring shape surfaces the narrower
4380 /// [`ManifestError::EtiquetaEmpty`] (the structural "this entry
4381 /// has no value" defect) before either the shape or the duplicate
4382 /// diagnostic. Walks the list in declaration order so the
4383 /// first-collision diagnostic surfaces the lexicographically-
4384 /// earliest offending position, peer with every other duplicate
4385 /// gate on this surface.
4386 ///
4387 /// Universal-axis (every kind carries `:etiquetas`), so wired at the
4388 /// caixa-build gate alongside the peer universal gates
4389 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4390 /// [`Self::validate_deps`] / [`Self::validate_code_paths`] — before
4391 /// the kind-coherence gates ([`crate::LayoutError::MeshSlotsOnNonAplicacao`]
4392 /// / [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4393 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4394 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-specific
4395 /// slot sets. The future caixa-registry search axis can reach for
4396 /// `caixa.etiquetas` knowing every entry is a non-empty distinct
4397 /// chart-keyword-shaped string without re-deriving the precondition.
4398 pub fn validate_etiquetas(&self) -> Result<(), ManifestError> {
4399 let mut seen = std::collections::HashSet::new();
4400 for etiqueta in self.etiquetas() {
4401 if etiqueta.is_empty() {
4402 return Err(ManifestError::EtiquetaEmpty);
4403 }
4404 crate::render::is_chart_keyword_shape(etiqueta).map_err(|reason| {
4405 ManifestError::EtiquetaInvalid {
4406 etiqueta: etiqueta.clone(),
4407 reason,
4408 }
4409 })?;
4410 crate::render::insert_first_seen(&mut seen, etiqueta.as_str(), || {
4411 ManifestError::EtiquetaDuplicate {
4412 etiqueta: etiqueta.clone(),
4413 }
4414 })?;
4415 }
4416 Ok(())
4417 }
4418
4419 /// Reject `:autores` lists with an empty entry or with two entries
4420 /// agreeing on the same string. `:autores` is the universal
4421 /// maintainer-axis on [`Caixa`] (every kind carries the
4422 /// `Vec<String>` slot) and lands verbatim as the Helm chart
4423 /// `Chart.yaml` `maintainers:` array on every Servico (caixa-helm's
4424 /// `build_chart_yaml` at `caixa-helm/src/lib.rs:251` maps each entry
4425 /// to a `Maintainer { name, email: None }` without dedup). Two
4426 /// authoring footguns silently passed validate without this gate:
4427 ///
4428 /// - Empty entry (`(:autores (""))` — the canonical paste-from-
4429 /// blank-doc footgun) rendered as
4430 /// `maintainers: [{name: "", email: null}]` in `Chart.yaml`. The
4431 /// empty maintainer name has no operational meaning — it
4432 /// identifies no one in the substrate's authorship index and
4433 /// clutters the rendered chart with a no-op maintainer.
4434 /// - Duplicate entries (`(:autores ("pleme-io" "pleme-io"))` —
4435 /// the copy-paste-the-wrong-author footgun) silently passed
4436 /// validate and rendered as two identical maintainer entries.
4437 /// Unlike the [`Self::validate_etiquetas`] peer (caixa-helm's
4438 /// `BTreeSet`-collect on `:etiquetas` silently dedups the
4439 /// rendered `keywords:` array at chart-render time), the
4440 /// `maintainers:` rendering has *no* dedup — duplicate `:autores`
4441 /// entries stack verbatim in the chart, divergent from every
4442 /// peer typed-graph set gate ([`crate::AplicacaoError::MembroDuplicate`]
4443 /// on `:membros`, [`crate::AplicacaoError::PlacementClusterDuplicate`]
4444 /// on `:placement :clusters`, [`crate::AplicacaoError::EntradaPathDuplicate`]
4445 /// on `:entrada :paths`, [`crate::AplicacaoError::ContratoDuplicate`]
4446 /// on `:contratos`, [`crate::DepError::DuplicateNome`] on
4447 /// `:deps` / `:deps-dev`, [`crate::UpgradeError::DuplicateFrom`]
4448 /// on `:upgrade-from`, [`ManifestError::EtiquetaDuplicate`] on
4449 /// `:etiquetas`).
4450 ///
4451 /// Past the empty arm the gate enforces the chart-maintainer-name
4452 /// shape predicate via [`crate::render::is_chart_maintainer_name_shape`]:
4453 /// the structural single-line printable-UTF-8 floor every realistic
4454 /// Helm chart maintainer name carries — 1..=128 bytes, no leading
4455 /// or trailing whitespace, no ASCII control characters anywhere,
4456 /// Unicode bytes accepted. Closes the canonical paste-from-doc
4457 /// footguns the bare empty + duplicate arms left open:
4458 /// paste-from-aligned-doc whitespace (`" pleme-io"`, `"pleme-io "`),
4459 /// paste-from-multiline-doc newline (`"alice\nbob"` — the author
4460 /// pasted a multi-line block of author records into one `:autores`
4461 /// entry instead of splitting into one entry per author),
4462 /// paste-from-Windows-CRLF-doc carriage return, tab-from-aligned-doc,
4463 /// and the paste-from-binary-blob control bytes that would silently
4464 /// land as YAML-illegal byte sequences in the rendered Chart.yaml
4465 /// `maintainers:` array. Mirrors the shape-predicate cascade
4466 /// [`Self::validate_descricao`] / [`Self::validate_licenca`] /
4467 /// [`Self::validate_edicao`] / [`Self::validate_repositorio`]
4468 /// establish past their own empty arms on the sibling universal-axis
4469 /// `Option<String>` surfaces — the first universal-axis Vec<String>
4470 /// surface to land the empty-first-then-shape-then-duplicate per-entry
4471 /// cascade.
4472 ///
4473 /// Same empty-first cascade discipline every peer per-axis gate
4474 /// uses: the per-entry empty arm fires before the per-entry shape
4475 /// arm before the cross-entry duplicate arm. Walks the list in
4476 /// declaration order so the first-collision diagnostic surfaces the
4477 /// lexicographically-earliest offending position, peer with every
4478 /// other duplicate gate on this surface.
4479 ///
4480 /// Universal-axis (every kind carries `:autores`), so wired at the
4481 /// caixa-build gate alongside the peer universal gates
4482 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4483 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
4484 /// [`Self::validate_code_paths`] — before the kind-coherence gates
4485 /// ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4486 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4487 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4488 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-specific
4489 /// slot sets.
4490 pub fn validate_autores(&self) -> Result<(), ManifestError> {
4491 let mut seen = std::collections::HashSet::new();
4492 for autor in self.autores() {
4493 if autor.is_empty() {
4494 return Err(ManifestError::AutorEmpty);
4495 }
4496 crate::render::is_chart_maintainer_name_shape(autor).map_err(|reason| {
4497 ManifestError::AutorInvalid {
4498 autor: autor.clone(),
4499 reason,
4500 }
4501 })?;
4502 crate::render::insert_first_seen(&mut seen, autor.as_str(), || {
4503 ManifestError::AutorDuplicate {
4504 autor: autor.clone(),
4505 }
4506 })?;
4507 }
4508 Ok(())
4509 }
4510
4511 /// Reject `:repositorio` values whose shape the shared
4512 /// [`crate::render::is_git_repo_url`] predicate refuses. The flat
4513 /// `repositorio: Option<String>` slot on [`Caixa`] is the
4514 /// universal git-shaped homepage axis every kind carries — the
4515 /// substrate routes the same string through two load-bearing
4516 /// consumers:
4517 ///
4518 /// - [`caixa-helm`] folds it verbatim into the rendered
4519 /// `lareira-<nome>` Helm chart's `Chart.yaml` `home:` field
4520 /// (`build_chart_yaml` at `caixa-helm/src/lib.rs:268`) and into
4521 /// the chart `README.md` `repo = …` interpolation
4522 /// (`caixa-helm/src/lib.rs:359`).
4523 /// - [`caixa-flux`] folds it verbatim into the standalone
4524 /// `ClusterBundleOpts::for_caixa` `git_url:` field
4525 /// (`caixa-flux/src/lib.rs:293`), which becomes the `FluxCD`
4526 /// `GitRepository.spec.url` the cluster's source-controller
4527 /// polls — the load-bearing deploy-time axis.
4528 ///
4529 /// Both consumers use `Option::unwrap_or_else(|| <fallback>)` to
4530 /// substitute a placeholder when the slot is absent (`None` → the
4531 /// fallback fires); a `Some("")` *skips the fallback* and silently
4532 /// passes the empty string through to `Chart.yaml home: ""` /
4533 /// `GitRepository url: ""` — Helm's chart lint and `FluxCD`'s source
4534 /// controller both reject the empty URL far from the source
4535 /// `caixa.lisp`, with no field naming the offending `:repositorio`.
4536 /// Similarly a malformed `:repositorio` (whitespace, control char,
4537 /// missing `:` separator, leading `-`) silently lands in the
4538 /// rendered artifacts and breaks at `git clone` / `helm template`
4539 /// / `flux reconcile` time.
4540 ///
4541 /// Thin wrapper around [`crate::render::is_git_repo_url`] — the
4542 /// same shared predicate the peer [`crate::DepSource::validate`]
4543 /// routes the `:fonte (:tipo git :repo …)` axis through. With this
4544 /// gate the two `git URL`-shaped surfaces on the typed Caixa
4545 /// (`:repositorio` here, `:deps :fonte :repo` peer) are
4546 /// structurally equivalent: every value past validate is
4547 /// guaranteed-acceptable by the predicate's union of constraints
4548 /// (non-empty, length-bounded, no leading `-`, no whitespace, no
4549 /// control chars, ASCII only, no leading `:`, contains a `:`
4550 /// separator). The predicate accepts every documented authoring
4551 /// shape — `github:org/repo` shorthand, `https://host/path`,
4552 /// `ssh://[user@]host/path`, `git://host/path`, `git@host:path`
4553 /// scp-style SSH, `file:///path` — and refuses the canonical
4554 /// paste-from-blank-doc / paste-from-multiline-doc / CLI-arg-
4555 /// injection footguns at validate time. Maps the predicate's
4556 /// `String` reason verbatim into the
4557 /// [`ManifestError::RepositorioInvalid`] variant, carrying the
4558 /// offending value + parser-shaped reason so the diagnostic is
4559 /// self-locating (the author can grep their `caixa.lisp` for
4560 /// `:repositorio "<value>"` and fix it in one edit).
4561 ///
4562 /// `None` (the canonical "omit the slot to express no published
4563 /// homepage" shape) is accepted trivially — the gate is a no-op
4564 /// when the author didn't declare a value. `Some("")` is gated by
4565 /// the narrower [`ManifestError::RepositorioEmpty`] arm before the
4566 /// shape predicate is consulted, mirroring the empty-first cascade
4567 /// every peer per-axis identity gate uses
4568 /// ([`ManifestError::NomeEmpty`] → [`ManifestError::NomeInvalid`],
4569 /// [`ManifestError::VersaoEmpty`] → [`ManifestError::VersaoInvalid`],
4570 /// [`crate::DepError::FonteRepoEmpty`] →
4571 /// [`crate::DepError::FonteRepoInvalid`]).
4572 ///
4573 /// Universal-axis (every kind carries `:repositorio`), so wired at
4574 /// the caixa-build gate alongside the peer universal gates
4575 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4576 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
4577 /// [`Self::validate_autores`] / [`Self::validate_code_paths`] —
4578 /// before the kind-coherence gates
4579 /// ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4580 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4581 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4582 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-
4583 /// specific slot sets.
4584 pub fn validate_repositorio(&self) -> Result<(), ManifestError> {
4585 let Some(s) = self.repositorio() else {
4586 return Ok(());
4587 };
4588 if s.is_empty() {
4589 return Err(ManifestError::RepositorioEmpty);
4590 }
4591 is_git_repo_url(s).map_err(|reason| ManifestError::RepositorioInvalid {
4592 repositorio: s.to_string(),
4593 reason,
4594 })
4595 }
4596
4597 /// Reject `:descricao` values that are the empty string. The flat
4598 /// `descricao: Option<String>` slot on [`Caixa`] is the universal
4599 /// free-form-prose homepage axis every kind carries — the
4600 /// substrate routes the same string through two load-bearing
4601 /// consumers in the [`caixa-helm`] renderer:
4602 ///
4603 /// - `build_chart_yaml` folds it verbatim into the rendered
4604 /// `lareira-<nome>` Helm chart's `Chart.yaml` `description:`
4605 /// field (`caixa-helm/src/lib.rs:232-235`).
4606 /// - `build_readme` folds it verbatim into the rendered chart
4607 /// `README.md` header (`caixa-helm/src/lib.rs:333-336`).
4608 ///
4609 /// Both consumers use `Option::unwrap_or_else(|| <fallback>)` to
4610 /// substitute a `caixa.nome`-derived placeholder when the slot is
4611 /// absent (`None` → the fallback fires); a `Some("")` *skips the
4612 /// fallback* and silently passes the empty string through to
4613 /// `Chart.yaml description: ""` / a blank chart `README.md`
4614 /// header. Helm's chart spec requires a non-empty `description:`
4615 /// field on `apiVersion: v2` charts (`helm lint` surfaces it as
4616 /// `WARNING [chart.metadata.description]: description is required`),
4617 /// so the empty `Some("")` silently lands in the rendered
4618 /// artifacts and breaks at `helm lint` / `helm install` time far
4619 /// from the source `caixa.lisp`, with no field naming the
4620 /// offending `:descricao`.
4621 ///
4622 /// `None` (the canonical "omit the slot to defer to the renderer's
4623 /// `caixa.nome`-derived fallback" shape) is accepted trivially —
4624 /// the gate is a no-op when the author didn't declare a value.
4625 /// `Some("")` is gated by the narrower
4626 /// [`ManifestError::DescricaoEmpty`] arm, mirroring the empty-arm
4627 /// shape every peer per-axis empty gate uses
4628 /// ([`ManifestError::NomeEmpty`], [`ManifestError::VersaoEmpty`],
4629 /// [`ManifestError::EtiquetaEmpty`], [`ManifestError::AutorEmpty`],
4630 /// [`ManifestError::RepositorioEmpty`]).
4631 ///
4632 /// Universal-axis (every kind carries `:descricao`), so wired at
4633 /// the caixa-build gate alongside the peer universal gates
4634 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4635 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
4636 /// [`Self::validate_autores`] / [`Self::validate_repositorio`] /
4637 /// [`Self::validate_code_paths`] — before the kind-coherence
4638 /// gates ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4639 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4640 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4641 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-
4642 /// specific slot sets.
4643 ///
4644 /// Past the empty arm the gate enforces the chart-description
4645 /// shape predicate via [`crate::render::is_chart_description_shape`]:
4646 /// the structural single-line UTF-8 floor every realistic chart
4647 /// description in the wild matches — 1..=512 bytes, no leading
4648 /// or trailing whitespace, no ASCII control characters anywhere
4649 /// (`0x00..=0x1F` plus `0x7F` DEL — banning tab, newline,
4650 /// carriage return, and every other control byte), Unicode
4651 /// continuation bytes accepted (the canonical fixtures carry
4652 /// `→` and `—`). Closes the canonical paste-from-doc footguns
4653 /// the bare empty-arm gate left open: paste-from-aligned-doc
4654 /// leading / trailing whitespace (`" Checkout flow."`,
4655 /// `"Checkout flow. "`), paste-from-multiline-doc newline
4656 /// (`"Checkout\nflow."`), paste-from-Windows-CRLF-doc CR
4657 /// (`"Checkout\rflow."`), tab-from-aligned-doc
4658 /// (`"Checkout\tflow."`), and paste-from-binary-blob NUL / BEL /
4659 /// ESC / DEL bytes. Mirrors the shape-predicate cascade
4660 /// [`Self::validate_repositorio`] / [`Self::validate_licenca`] /
4661 /// [`Self::validate_edicao`] establish past their own empty arms
4662 /// on the sibling universal-axis `Option<String>` Caixa-level
4663 /// value-shape surfaces.
4664 ///
4665 /// The empty-first cascade discipline mirrors every peer per-axis
4666 /// identity gate: [`ManifestError::DescricaoEmpty`] runs before
4667 /// [`ManifestError::DescricaoInvalid`], so the narrower empty
4668 /// diagnostic surfaces on `Some("")` rather than the broader
4669 /// shape-predicate diagnostic — peer with how
4670 /// [`ManifestError::LicencaEmpty`] runs before
4671 /// [`ManifestError::LicencaInvalid`],
4672 /// [`ManifestError::EdicaoEmpty`] runs before
4673 /// [`ManifestError::EdicaoInvalid`],
4674 /// [`ManifestError::RepositorioEmpty`] runs before
4675 /// [`ManifestError::RepositorioInvalid`].
4676 pub fn validate_descricao(&self) -> Result<(), ManifestError> {
4677 let Some(s) = self.descricao() else {
4678 return Ok(());
4679 };
4680 if s.is_empty() {
4681 return Err(ManifestError::DescricaoEmpty);
4682 }
4683 crate::render::is_chart_description_shape(s).map_err(|reason| {
4684 ManifestError::DescricaoInvalid {
4685 descricao: s.to_string(),
4686 reason,
4687 }
4688 })?;
4689 Ok(())
4690 }
4691
4692 /// Reject `:licenca` values that are the empty string. The flat
4693 /// `licenca: Option<String>` slot on [`Caixa`] is the universal
4694 /// SPDX-shaped license-expression axis every kind carries — the
4695 /// substrate routes the same string through the [`caixa-helm`]
4696 /// renderer's `build_readme` which folds it verbatim into the
4697 /// rendered `lareira-<nome>` Helm chart's `README.md` `## License`
4698 /// section (`caixa-helm/src/lib.rs:361`) via
4699 /// `caixa.licenca.clone().unwrap_or_else(|| "MIT".into())`. The
4700 /// fallback only fires on `None`; a `Some("")` *skips the
4701 /// fallback* and silently passes the empty string through to a
4702 /// chart `README.md` whose `License` section renders as the bare
4703 /// trailing period (`.\n`) — peer footgun with the
4704 /// `Some("")`-skips-`unwrap_or_else` shape the
4705 /// [`Self::validate_descricao`] and [`Self::validate_repositorio`]
4706 /// gates close on the sibling free-form-prose and git-URL axes.
4707 ///
4708 /// `None` (the canonical "omit the slot to defer to the
4709 /// renderer's `MIT` fallback" shape every existing fixture
4710 /// carries) is accepted trivially — the gate is a no-op when the
4711 /// author didn't declare a value. `Some("")` is gated by the
4712 /// narrower [`ManifestError::LicencaEmpty`] arm, mirroring the
4713 /// empty-arm shape every peer per-axis empty gate uses
4714 /// ([`ManifestError::NomeEmpty`], [`ManifestError::VersaoEmpty`],
4715 /// [`ManifestError::EtiquetaEmpty`], [`ManifestError::AutorEmpty`],
4716 /// [`ManifestError::RepositorioEmpty`],
4717 /// [`ManifestError::DescricaoEmpty`]).
4718 ///
4719 /// Universal-axis (every kind carries `:licenca`), so wired at
4720 /// the caixa-build gate alongside the peer universal gates
4721 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4722 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
4723 /// [`Self::validate_autores`] / [`Self::validate_repositorio`] /
4724 /// [`Self::validate_descricao`] / [`Self::validate_code_paths`]
4725 /// — before the kind-coherence gates
4726 /// ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4727 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4728 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4729 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-
4730 /// specific slot sets.
4731 ///
4732 /// Past the empty arm the gate enforces the SPDX-expression shape
4733 /// predicate via [`crate::render::is_spdx_expression_shape`]: the
4734 /// structural alphabet floor every realistic SPDX expression in
4735 /// the wild uses — ASCII alphanumeric plus `.`, `-`, `+`, `(`,
4736 /// `)`, `:` (the `DocumentRef-…:LicenseRef-…` separator), and a
4737 /// single ASCII space (token separator). Closes the canonical
4738 /// paste-from-doc footguns the bare empty-arm gate left open:
4739 /// paste-from-doc whitespace (`"MIT "`, `" MIT"`), paste-from-
4740 /// multiline-doc CRLF (`"MIT\n"`), tab-from-aligned-doc
4741 /// (`"MIT\tOR Apache-2.0"`), non-ASCII smart-quote paste,
4742 /// underscore-instead-of-hyphen typo (`"Apache_2.0"`),
4743 /// comma-instead-of-`OR`-keyword colloquial idiom (`"MIT,
4744 /// Apache-2.0"`), slash-dual-license colloquial idiom (`"MIT/
4745 /// Apache-2.0"`), and semicolon-list-separator confusion
4746 /// (`"MIT; Apache-2.0"`). Mirrors the shape-predicate cascade
4747 /// [`Self::validate_repositorio`] / [`Self::validate_edicao`]
4748 /// establish past their own empty arms.
4749 ///
4750 /// The empty-first cascade discipline mirrors every peer per-axis
4751 /// identity gate: [`ManifestError::LicencaEmpty`] runs before
4752 /// [`ManifestError::LicencaInvalid`], so the narrower empty
4753 /// diagnostic surfaces on `Some("")` rather than the broader
4754 /// shape-predicate diagnostic — peer with how
4755 /// [`ManifestError::EdicaoEmpty`] runs before
4756 /// [`ManifestError::EdicaoInvalid`],
4757 /// [`ManifestError::RepositorioEmpty`] runs before
4758 /// [`ManifestError::RepositorioInvalid`].
4759 ///
4760 /// A future tightening on this axis can extend the alphabet
4761 /// floor into a full SPDX expression parser + license-id
4762 /// allowlist (rejecting alphabet-valid values that don't name a
4763 /// real SPDX license identifier — e.g., `"NotAReal"` is
4764 /// alphabet-valid but no `NotAReal` license-id exists). That
4765 /// parser only becomes meaningful past a real SPDX-spec
4766 /// dependency; this gate establishes the structural floor by
4767 /// refusing every non-SPDX-alphabet value at validate time.
4768 pub fn validate_licenca(&self) -> Result<(), ManifestError> {
4769 let Some(s) = self.licenca() else {
4770 return Ok(());
4771 };
4772 if s.is_empty() {
4773 return Err(ManifestError::LicencaEmpty);
4774 }
4775 crate::render::is_spdx_expression_shape(s).map_err(|reason| {
4776 ManifestError::LicencaInvalid {
4777 licenca: s.to_string(),
4778 reason,
4779 }
4780 })?;
4781 Ok(())
4782 }
4783
4784 /// Reject `:edicao` values that are the empty string. The flat
4785 /// `edicao: Option<String>` slot on [`Caixa`] is the universal
4786 /// language-edition axis every kind carries — it determines the
4787 /// tatara-lisp macro surface + compatibility flags the substrate
4788 /// applies when building a caixa, and lands verbatim in the
4789 /// `Caixa::template` author-time scaffold (the canonical
4790 /// `:edicao "2026"` line every `feira init` emits via
4791 /// [`Caixa::template`] at `caixa-core/src/manifest.rs:1193`) and
4792 /// in every renderer-side fixture (`caixa-helm/src/lib.rs:375`,
4793 /// `caixa-flux/src/lib.rs:445`, `caixa-mesh/src/lib.rs:629`,
4794 /// `caixa-core/src/render.rs:2510`) via
4795 /// `edicao: Some("2026".into())`.
4796 ///
4797 /// `None` (the canonical "omit the slot to defer to the
4798 /// substrate's default edition" shape every existing
4799 /// [`caixa-resolver`] integration test fixture carries via
4800 /// `edicao: None` — see `caixa-resolver/tests/git_integration.rs`)
4801 /// is accepted trivially — the gate is a no-op when the author
4802 /// didn't declare a value. `Some("")` is gated by the narrower
4803 /// [`ManifestError::EdicaoEmpty`] arm, mirroring the empty-arm
4804 /// shape every peer per-axis empty gate uses
4805 /// ([`ManifestError::NomeEmpty`], [`ManifestError::VersaoEmpty`],
4806 /// [`ManifestError::EtiquetaEmpty`], [`ManifestError::AutorEmpty`],
4807 /// [`ManifestError::RepositorioEmpty`],
4808 /// [`ManifestError::DescricaoEmpty`], [`ManifestError::LicencaEmpty`]).
4809 ///
4810 /// Universal-axis (every kind carries `:edicao`), so wired at
4811 /// the caixa-build gate alongside the peer universal gates
4812 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4813 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
4814 /// [`Self::validate_autores`] / [`Self::validate_repositorio`] /
4815 /// [`Self::validate_descricao`] / [`Self::validate_licenca`] /
4816 /// [`Self::validate_code_paths`] — before the kind-coherence
4817 /// gates ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4818 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4819 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4820 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-
4821 /// specific slot sets.
4822 ///
4823 /// Past the empty arm the gate enforces the canonical year-shape
4824 /// predicate: every documented tatara-lisp edition is a 4-digit
4825 /// ASCII decimal year (`"2026"` is the only edition currently
4826 /// minted; future-introduced siblings will follow the same
4827 /// shape, peer with Cargo's `[package] edition` grammar which
4828 /// every value Cargo has ever accepted matches — `"2015"`,
4829 /// `"2018"`, `"2021"`, `"2024"`). Any value that's not exactly
4830 /// 4 ASCII decimal bytes is rejected with the narrower
4831 /// [`ManifestError::EdicaoInvalid`] arm, mirroring the
4832 /// shape-predicate cascade [`Self::validate_repositorio`]
4833 /// establishes past its own empty arm
4834 /// ([`ManifestError::RepositorioEmpty`] →
4835 /// [`ManifestError::RepositorioInvalid`]). Closes the canonical
4836 /// paste-from-doc footguns the bare empty-arm gate left open:
4837 ///
4838 /// - leading / trailing whitespace from a paste-from-doc
4839 /// (`"2026 "`, `" 2026"`)
4840 /// - control characters / CRLF from a paste-from-multiline-doc
4841 /// (`"2026\n"`)
4842 /// - non-ASCII look-alikes from a fullwidth keyboard
4843 /// (`"2026"`) which would silently land as a non-ASCII
4844 /// string in the rendered caixa.lisp
4845 /// - free-form non-year values (`"x"`, `"latest"`,
4846 /// `"nightly"`) that have no operational meaning on the
4847 /// substrate's build-time edition selector
4848 /// - leading non-digit prefixes (`"v2026"`, `"e2026"`,
4849 /// `"r2026"`) — common version-tag idioms that don't apply
4850 /// to the year-shaped edition axis
4851 /// - decimal-shaped values (`"2026.1"`, `"2026.0"`) — every
4852 /// edition is a year, not a fractional version
4853 /// - wrong-length numeric values (`"26"`, `"202"`, `"20260"`,
4854 /// `"00026"`) that don't name a year
4855 ///
4856 /// `None` (the canonical "omit the slot to defer to the
4857 /// substrate's default edition" shape every existing
4858 /// [`caixa-resolver`] integration test fixture carries via
4859 /// `edicao: None` — see `caixa-resolver/tests/git_integration.rs`)
4860 /// is accepted trivially — the gate is a no-op when the author
4861 /// didn't declare a value. The empty-first cascade discipline
4862 /// mirrors every peer per-axis identity gate:
4863 /// [`ManifestError::EdicaoEmpty`] runs before
4864 /// [`ManifestError::EdicaoInvalid`], so the narrower empty
4865 /// diagnostic surfaces on `Some("")` rather than the broader
4866 /// shape-predicate diagnostic — peer with how
4867 /// [`ManifestError::NomeEmpty`] runs before
4868 /// [`ManifestError::NomeInvalid`],
4869 /// [`ManifestError::VersaoEmpty`] runs before
4870 /// [`ManifestError::VersaoInvalid`],
4871 /// [`ManifestError::RepositorioEmpty`] runs before
4872 /// [`ManifestError::RepositorioInvalid`].
4873 ///
4874 /// A future tightening on this axis can extend the shape
4875 /// predicate into a known-edition allowlist (rejecting
4876 /// year-shaped values that don't name a tatara-lisp edition
4877 /// the substrate actually understands — e.g., `"1999"` is
4878 /// year-shaped but no `1999` edition exists). That allowlist
4879 /// only becomes meaningful past the introduction of a sibling
4880 /// edition to `"2026"`; this gate establishes the structural
4881 /// floor by refusing every non-year-shaped value at validate
4882 /// time.
4883 pub fn validate_edicao(&self) -> Result<(), ManifestError> {
4884 let Some(s) = self.edicao() else {
4885 return Ok(());
4886 };
4887 if s.is_empty() {
4888 return Err(ManifestError::EdicaoEmpty);
4889 }
4890 if s.len() != 4 || !s.bytes().all(|b| b.is_ascii_digit()) {
4891 return Err(ManifestError::EdicaoInvalid {
4892 edicao: s.to_string(),
4893 reason: "must be a 4-digit ASCII decimal year (canonical \"2026\")".to_string(),
4894 });
4895 }
4896 Ok(())
4897 }
4898
4899 /// Compose the supervisor-related flat slots into a single
4900 /// [`SupervisorSpec`] for validation. Returns `None` when the
4901 /// caixa isn't a `:kind Supervisor`.
4902 ///
4903 /// The flat representation in [`Caixa`] keeps tatara-lisp authoring
4904 /// simple (one form, no nested `:supervisor (…)` block); this view
4905 /// is the "typed shape" the operator + supervisor reconciler
4906 /// consume.
4907 #[must_use]
4908 pub fn supervisor_view(&self) -> Option<SupervisorSpec> {
4909 if !self.kind().is_supervisor() {
4910 return None;
4911 }
4912 // Fold through the shared `supervisor::duration_codec::parse`
4913 // — the same parser the serde-routed `with = "duration_codec"`
4914 // on `SupervisorSpec::restart_window`, the `:politicas
4915 // :timeout` codec, and the `:politicas :circuit-breaker
4916 // :window` codec all consume. The prior inline f64-shaped
4917 // duplicate (`parse_window_inline`) admitted every magnitude
4918 // the integer-magnitude gate (1c55a2a) rejects on the three
4919 // serde-routed siblings — `"1.5s"`, `"1.0s"`, `"0.5m"`,
4920 // `"+30s"`, `"-30s"` — and silently dropped malformed input as
4921 // `None` (i.e. "no reset"), divergent from the shared codec's
4922 // integer-magnitude discipline by construction. The fold
4923 // closes the divergence: every value the typed
4924 // `SupervisorSpec` carries past `supervisor_view` is in the
4925 // shared codec's accepted set. The `.ok()` here preserves the
4926 // existing soft-swallow shape on this view-construction path;
4927 // the new [`Caixa::validate_restart_window`] (sibling of
4928 // [`Self::validate_nome`] / [`Self::validate_versao`]) names
4929 // the offending raw string at build time so authoring tools
4930 // (`feira lint`, the future layout-side wire-up) surface a
4931 // self-locating diagnostic instead of a silently dropped
4932 // window.
4933 let restart_window = self
4934 .restart_window()
4935 .and_then(|s| crate::supervisor::duration_codec::parse(s).ok());
4936 Some(SupervisorSpec {
4937 estrategia: self.estrategia().unwrap_or_default(),
4938 max_restarts: self.max_restarts().unwrap_or(5),
4939 restart_window,
4940 children: self.children().to_vec(),
4941 })
4942 }
4943
4944 /// A minimal starter manifest emitted by `feira init`.
4945 #[must_use]
4946 pub fn template(nome: &str) -> String {
4947 format!(
4948 "(defcaixa\n \
4949 :nome {nome:?}\n \
4950 :versao \"0.1.0\"\n \
4951 :kind Biblioteca\n \
4952 :edicao \"2026\"\n \
4953 :descricao \"FIXME — describe this caixa\"\n \
4954 :autores ()\n \
4955 :etiquetas ()\n \
4956 :deps ()\n \
4957 :deps-dev ()\n \
4958 :bibliotecas (\"lib/{nome}.lisp\"))\n"
4959 )
4960 }
4961
4962 /// Serialize to a canonical `caixa.lisp` source — suitable for writing
4963 /// back after mutation (e.g. `feira add`).
4964 ///
4965 /// Goes through serde JSON → canonical Sexp → per-field pretty print.
4966 /// The derive-macro `compile_from_sexp` path is the inverse, so any
4967 /// `Caixa` round-trips through `to_lisp` + `from_lisp`.
4968 #[must_use]
4969 pub fn to_lisp(&self) -> String {
4970 let json = serde_json::to_value(self).expect("Caixa serialize");
4971 let sexp = tatara_lisp::domain::json_to_sexp(&json);
4972 let tatara_lisp::Sexp::List(items) = sexp else {
4973 return format!("(defcaixa {sexp})\n");
4974 };
4975 let mut out = String::from("(defcaixa");
4976 let mut i = 0;
4977 while i + 1 < items.len() {
4978 out.push_str("\n ");
4979 out.push_str(&items[i].to_string());
4980 out.push(' ');
4981 out.push_str(&items[i + 1].to_string());
4982 i += 2;
4983 }
4984 out.push_str(")\n");
4985 out
4986 }
4987}
4988
4989/// Errors raised by top-level [`Caixa`] validators that don't fit
4990/// the per-axis [`DepError`] / [`crate::AplicacaoError`] /
4991/// [`crate::SupervisorError`] / [`crate::LayoutError`] families —
4992/// the Caixa's own identity axes (`:nome`, `:versao`) that flow
4993/// through every substrate-side artifact's `metadata.name` /
4994/// version derivation.
4995///
4996/// A future top-level sum (the M4 `CaixaError` the [`DepError`]
4997/// doc-comment anticipates) can hold one of each per-axis error
4998/// family without reshaping individual diagnostics; this enum is
4999/// the first such per-Caixa-identity family.
5000#[derive(Debug, Error, PartialEq, Eq)]
5001pub enum ManifestError {
5002 #[error(
5003 ":nome is empty (every caixa must name itself; the value flows \
5004 into every K8s artifact's `metadata.name` derivation and into \
5005 the default `lib/<nome>.lisp` / `exe/<nome>` layout paths)"
5006 )]
5007 NomeEmpty,
5008 #[error(
5009 ":nome {nome:?} is not a valid DNS-1123 label: {reason} (the K8s \
5010 apiserver enforces this rule on every `metadata.name` the \
5011 caixa's substrate-side renderers derive from `:nome` — the \
5012 `lareira-<nome>` Helm chart name, the programs.yaml entry \
5013 name, the `LABEL_APLICACAO` label value, the `<aplicacao>-<de>-to-<para>` \
5014 CiliumNetworkPolicy name, the `<aplicacao>-<para>` HTTPRoute \
5015 name; use a lowercase alphanumeric + hyphen identifier like \
5016 `\"checkout\"` or `\"cart-v2\"`)"
5017 )]
5018 NomeInvalid { nome: String, reason: String },
5019 #[error(
5020 ":nome {nome:?} overflows the joint-length budget on the canonical \
5021 `lareira-<nome>` chart-name shape: {reason} (every per-Servico / \
5022 per-Aplicacao renderer the substrate carries — `caixa-helm`'s \
5023 `Chart.yaml::name`, `caixa-flux`'s `cluster_bundle` HelmRelease \
5024 `chart:` slot, `caixa-tatara`'s `release_name` + \
5025 `oci://<registry>/lareira-<nome>` chart ref — derives the same \
5026 joint name through the canonical `lareira_chart_name` helper, and \
5027 Helm's `Chart.yaml::name` admission rule + the K8s apiserver's \
5028 DNS-1123 label cap on every chart-name-derived `metadata.name` \
5029 reject any joint name exceeding 63 bytes; the narrower \
5030 `:nome` shape (`NomeInvalid`) gates the bare-`:nome` budget, this \
5031 arm gates the chart-name budget downstream renderers inherit)"
5032 )]
5033 NomeChartNameBudgetExceeded { nome: String, reason: String },
5034 #[error(
5035 ":versao is empty (every caixa must pin its own version; the value flows \
5036 into the `lareira-<nome>` Helm chart's `Chart.yaml` version + appVersion, \
5037 the `feira publish` `v<versao>` git tag, the OCI image's `:v<versao>` / \
5038 `:latest` tags, the lacre closure's `concrete_versao`, and the \
5039 `:upgrade-from :from` peers — use a SemVer-2 literal like `\"0.1.0\"`)"
5040 )]
5041 VersaoEmpty,
5042 #[error(
5043 ":versao {versao:?} is not a valid SemVer-2 version: {reason} (the substrate \
5044 consumes this string as `semver::Version` — three-part `MAJOR.MINOR.PATCH` \
5045 with optional `-prerelease` and `+build` — across every artifact derived \
5046 from `:versao`: the `lareira-<nome>` Helm chart's `Chart.yaml` version + \
5047 appVersion (Helm SemVer-2-strict), the `feira publish` `v<versao>` git tag, \
5048 the OCI image's `:v<versao>` tag, the lacre closure's `concrete_versao`, \
5049 and the `:upgrade-from :from` peers that match against this exact shape; \
5050 use a literal like `\"0.1.0\"`, `\"0.2.0-rc.1\"`, or `\"1.0.0+build.42\"` — \
5051 not a git-tag-shape like `\"v0.1.0\"`, a docker-tag-shape like `\"latest\"`, \
5052 a requirement-shape like `\"^0.1\"`, or a four-part `\"0.1.0.0\"`)"
5053 )]
5054 VersaoInvalid { versao: String, reason: String },
5055 #[error(
5056 ":restart-window {restart_window:?} is not a valid duration: {reason} (the \
5057 substrate consumes this string through the shared \
5058 `supervisor::duration_codec` — the same parser routed via `with = \
5059 \"duration_codec\"` onto the typed `SupervisorSpec::restart_window`, \
5060 `:politicas :timeout`, and `:politicas :circuit-breaker :window` slots; \
5061 the canonical authoring form is `<integer><unit>` where the unit is one \
5062 of `ms` / `s` / `m` / `h` and the magnitude has no decimal point and no \
5063 leading `+` / `-` sign — e.g. `\"60s\"`, `\"5m\"`, `\"1h\"`, `\"500ms\"`. \
5064 Without this gate a malformed `:restart-window` silently produced a \
5065 supervisor with `restart_window: None` (\"never reset\"), turning OTP's \
5066 `MaxIntensity / Period` invariant into a never-reset supervisor far from \
5067 the source `caixa.lisp`; the gate moves the diagnostic to the manifest \
5068 layer with the offending value named verbatim. Omit the slot entirely to \
5069 express \"no reset\"; carry a positive integer duration to express the \
5070 sliding window)"
5071 )]
5072 RestartWindowMalformed {
5073 restart_window: String,
5074 reason: String,
5075 },
5076 #[error(
5077 "{slot} entry is an empty path string — every {slot} entry must name \
5078 a file relative to the caixa root; omit the entry to omit the file \
5079 (the layout checker's `root.join(\"\")` resolves to the caixa root \
5080 itself, so an empty entry silently aliases the project root as a \
5081 declared {slot} file, then fails downstream at parse / existence \
5082 time with a diagnostic that names the root rather than the offending \
5083 entry)"
5084 )]
5085 CodePathEmpty { slot: &'static str },
5086 #[error(
5087 "{slot} entry {} is an absolute path — entries must be relative to \
5088 the caixa root, since `Path::join` replaces the base with an absolute \
5089 right-hand side and `root.join(\"/abs/...\")` resolves to \"/abs/...\" \
5090 outside the caixa root sandbox; rewrite the entry as a relative path \
5091 under the caixa root (e.g. `\"lib/<name>.lisp\"`, `\"exe/<name>\"`, \
5092 `\"servicos/<name>.computeunit.yaml\"`)",
5093 path.display()
5094 )]
5095 CodePathAbsolute { slot: &'static str, path: PathBuf },
5096 #[error(
5097 "{slot} entry {} contains a `..` component — entries must not traverse \
5098 above the caixa root (the layout's `starts_with(<dir>)` fence on \
5099 `:exe` / `:servicos` is component-aware, not canonical-path-aware, \
5100 so a mid-path `..` silently traverses the sandbox; `:bibliotecas` \
5101 has no such fence, so a leading `..` escapes unconditionally if the \
5102 resolved target happens to exist)",
5103 path.display()
5104 )]
5105 CodePathParentEscape { slot: &'static str, path: PathBuf },
5106 #[error(
5107 "{slot} entry {} does not terminate in the `.lisp` extension — every \
5108 `:bibliotecas` entry is a tatara-lisp source file the `feira build` \
5109 loop reads through `tatara_lisp::read` at parse time, so any other \
5110 extension (`.rs`, `.txt`, `.lisp.bak`) or no-extension shape is \
5111 structurally a parser error far from the source caixa.lisp, with \
5112 no field naming the offending `:bibliotecas` entry. Pin a relative \
5113 path under the caixa root whose terminating extension is \
5114 lowercase-`.lisp` (e.g. `\"lib/<name>.lisp\"`, \
5115 `\"lib/handlers.lisp\"`) — the same file-type contract the peer \
5116 `:behavior :on-*` (c97815a) and `:upgrade-from :state-change :script` \
5117 (33cc830) axes already carry through the same lifted \
5118 `is_lisp_extension` predicate",
5119 path.display()
5120 )]
5121 CodePathNonLispExtension { slot: &'static str, path: PathBuf },
5122 #[error(
5123 "{slot} entry {} does not terminate in the `.computeunit.yaml` \
5124 compound suffix — every `:servicos` entry is a typed `ComputeUnit` \
5125 CR YAML file the peer caixa-helm / caixa-flux renderers consume \
5126 through `serde_yaml::from_str` at chart / FluxCD bundle render \
5127 time, so any other extension (`.yaml`, `.yml`, `.json`, the \
5128 off-by-one-segment `.computeunit-yaml`, the editor-backup \
5129 `.computeunit.yaml.bak`) or no-extension shape is structurally a \
5130 YAML-parser error / `ComputeUnit` schema-mismatch far from the \
5131 source caixa.lisp, with no field naming the offending `:servicos` \
5132 entry. Pin a relative path under the caixa root whose terminating \
5133 compound suffix is lowercase-`.computeunit.yaml` (e.g. \
5134 `\"servicos/<name>.computeunit.yaml\"`, \
5135 `\"servicos/hello-rio.computeunit.yaml\"`) — the same file-type \
5136 contract the sibling `:bibliotecas` axis (64772a9) already carries \
5137 on the tatara-lisp-source axis through the peer lifted \
5138 `is_lisp_extension` predicate, here on the compound-suffix axis \
5139 `Path::extension` can't express on its own through the lifted \
5140 `is_computeunit_yaml_extension` predicate",
5141 path.display()
5142 )]
5143 CodePathNonComputeUnitYamlExtension { slot: &'static str, path: PathBuf },
5144 #[error(
5145 "{slot} entry {} appears more than once (the code-path list is \
5146 a set, not a multiset; every peer Vec-shaped author-supplied \
5147 list past validate is set-not-multiset — `:membros :caixa`, \
5148 `:placement :clusters`, `:entrada :paths`, `:contratos`, \
5149 `:children :caixa`, `:deps` / `:deps-dev` `:nome`, \
5150 `:upgrade-from :from`, `:etiquetas`, `:autores` — and the three \
5151 code-path lists are the last Vec-shaped author-supplied slots on \
5152 the typed Caixa surface still admitting a duplicate entry. \
5153 `:bibliotecas` duplicates re-parse the same file at \
5154 `feira build` time and silently mask the author's intent to \
5155 declare a *second* biblioteca; `:exe` duplicates collide on the \
5156 flake `packages.<name>` derivation key at the future \
5157 `caixa-flake` materializer; `:servicos` duplicates surface as the \
5158 narrower [`caixa-helm`] / [`caixa-flux`] `UnsupportedServicoCount` \
5159 rejection far from the source `caixa.lisp`. Drop the duplicate \
5160 or rename it to the actual second file intended)",
5161 path.display()
5162 )]
5163 CodePathDuplicate { slot: &'static str, path: PathBuf },
5164 #[error(
5165 ":etiquetas entry is empty (every tag must carry a non-empty \
5166 registry-search identifier; the empty entry has no operational \
5167 meaning — it indexes nothing in the future caixa-registry search \
5168 axis and clutters the rendered Helm `Chart.yaml` `keywords:` array \
5169 with a no-op tag; omit the entry to express \"no tag on this \
5170 position\")"
5171 )]
5172 EtiquetaEmpty,
5173 #[error(
5174 ":etiquetas entry {etiqueta:?} appears more than once (the \
5175 registry-search tag set is a set, not a multiset; duplicate \
5176 entries are silently dedup'd by caixa-helm's `BTreeSet` collect \
5177 at chart render — a \"second wins / one silently disappears\" \
5178 shape divergent from every peer typed-graph set gate \
5179 (`:membros :caixa`, `:placement :clusters`, `:entrada :paths`, \
5180 `:contratos`, `:deps :nome`, `:upgrade-from :from`); drop the \
5181 duplicate or rename it to the actual tag intended)"
5182 )]
5183 EtiquetaDuplicate { etiqueta: String },
5184 #[error(
5185 ":etiquetas entry {etiqueta:?} is not a valid chart-keyword shape: \
5186 {reason} (the substrate consumes this string through the shared \
5187 `crate::render::is_chart_keyword_shape` predicate — the same \
5188 Cargo crates.io `[package] keywords` grammar entry shape: 1..=20 \
5189 bytes, starts with an ASCII letter, ASCII alphanumeric / `_` / `-` \
5190 continuation. The canonical authoring shapes are short kebab-case \
5191 identifiers like `\"mesh\"`, `\"wasm\"`, `\"tatara-lisp\"`, \
5192 `\"hello-world\"`, `\"caixa-servico\"`, `\"infrastructure\"`. \
5193 Without this gate a malformed `:etiquetas` entry (paste-from-doc \
5194 leading / trailing whitespace `\" mesh\"` / `\"mesh \"`; \
5195 paste-from-multiline-doc newline `\"mesh\\nhttp\"`; \
5196 paste-from-Windows-CRLF-doc CR; CSV-list-separator confusion \
5197 `\"mesh,http,grpc\"` — the author meant to author three separate \
5198 list entries; path-separator confusion `\"caixa/servico\"`; \
5199 namespace-suffix `\"http.1\"`; leading-digit `\"1foo\"`; \
5200 kebab-leak `\"-foo\"`; snake-leak `\"_foo\"`; non-ASCII \
5201 `\"café\"` — every legitimate search tag is strict ASCII; \
5202 paste-from-binary-blob NUL / BEL / ESC / DEL byte) silently \
5203 passed `from_lisp` + `validate_etiquetas` + \
5204 `StandardLayout::verify` and landed in the rendered \
5205 `lareira-<nome>` Helm chart's `Chart.yaml keywords:` array as a \
5206 malformed search tag — Artifact Hub's keyword index + the future \
5207 caixa-registry's keyword index would either silently drop the \
5208 tag or fail to index it far from the source caixa.lisp; the gate \
5209 moves the diagnostic to the manifest layer with the offending \
5210 value named verbatim)"
5211 )]
5212 EtiquetaInvalid { etiqueta: String, reason: String },
5213 #[error(
5214 ":autores entry is empty (every maintainer must carry a non-empty \
5215 identifier; the empty entry has no operational meaning — it \
5216 identifies no one in the substrate's authorship index and renders \
5217 as `maintainers: [{{name: \"\", email: null}}]` in the Helm chart's \
5218 `Chart.yaml`, a no-op maintainer the substrate cannot route to; \
5219 omit the entry to express \"no maintainer on this position\")"
5220 )]
5221 AutorEmpty,
5222 #[error(
5223 ":autores entry {autor:?} appears more than once (the maintainer \
5224 set is a set, not a multiset; unlike `:etiquetas`, caixa-helm's \
5225 `maintainers:` rendering does *no* dedup — duplicate entries \
5226 stack verbatim in `Chart.yaml` as two identical \
5227 `Maintainer {{ name, email: None }}` records, divergent from every \
5228 peer typed-graph set gate (`:etiquetas`, `:membros :caixa`, \
5229 `:placement :clusters`, `:entrada :paths`, `:contratos`, \
5230 `:deps :nome`, `:upgrade-from :from`); drop the duplicate or \
5231 rename it to the actual author intended)"
5232 )]
5233 AutorDuplicate { autor: String },
5234 #[error(
5235 ":autores entry {autor:?} is not a valid chart-maintainer-name shape: \
5236 {reason} (the substrate consumes this string through the shared \
5237 `crate::render::is_chart_maintainer_name_shape` predicate — the same \
5238 single-line-UTF-8 floor every realistic chart maintainer name carries: \
5239 1..=128 bytes, no leading or trailing whitespace, no ASCII control \
5240 characters anywhere, Unicode bytes accepted. The canonical authoring \
5241 shapes are short single-line identifiers like `\"pleme-io\"`, \
5242 `\"Pleme Contributors\"`, `\"alice <alice@example.com>\"`, \
5243 `\"François Dupont\"`. Without this gate a malformed `:autores` entry \
5244 (paste-from-aligned-doc leading whitespace `\" pleme-io\"` / trailing \
5245 whitespace `\"pleme-io \"`; paste-from-multiline-doc newline \
5246 `\"alice\\nbob\"` — the author pasted a multi-line block of author \
5247 records into one entry instead of splitting into one entry per author; \
5248 paste-from-Windows-CRLF-doc carriage return `\"alice\\rbob\"`; \
5249 tab-from-aligned-doc `\"Pleme\\tContributors\"`; paste-from-binary-blob \
5250 NUL / BEL / ESC / DEL byte) silently passed `from_lisp` + \
5251 `validate_autores` + `StandardLayout::verify` and landed in the \
5252 rendered `lareira-<nome>` Helm chart's `Chart.yaml maintainers:` array \
5253 as a YAML-illegal multi-line scalar or a silently-trimmed whitespace \
5254 round-trip — every chart-aware UI (`helm list`, `helm search`, \
5255 Artifact Hub maintainer index) would render the maintainer name in a \
5256 single-line column far from the source caixa.lisp; the gate moves the \
5257 diagnostic to the manifest layer with the offending value named \
5258 verbatim)"
5259 )]
5260 AutorInvalid { autor: String, reason: String },
5261 #[error(
5262 ":repositorio is the empty string (every published caixa names its \
5263 git source via a non-empty `:repositorio` locator — the value \
5264 flows verbatim into the rendered `lareira-<nome>` Helm chart's \
5265 `Chart.yaml` `home:` field via `caixa-helm` and into the FluxCD \
5266 `GitRepository.spec.url` via `caixa-flux`'s \
5267 `ClusterBundleOpts::for_caixa`; both consumers' \
5268 `Option::unwrap_or_else` fallbacks only fire when the slot is \
5269 `None`, so an empty `Some(\"\")` silently lands as `home: \"\"` / \
5270 `url: \"\"` in the rendered artifacts and breaks at `helm \
5271 template` / FluxCD source-controller reconcile time far from the \
5272 source caixa.lisp; omit the slot entirely to defer to the \
5273 renderer's `https://github.com/pleme-io/<nome>` / \
5274 `caixa.nome`-derived fallback, or carry a canonical authoring \
5275 shape like `\"github:org/repo\"`, `\"https://host/path\"`, \
5276 `\"ssh://[user@]host/path\"`, `\"git@host:path\"`, or \
5277 `\"file:///path\"`)"
5278 )]
5279 RepositorioEmpty,
5280 #[error(
5281 ":repositorio {repositorio:?} is not a valid git repo URL: {reason} \
5282 (the substrate consumes this string through the shared \
5283 `crate::render::is_git_repo_url` predicate — the same parser the \
5284 peer `:deps :fonte (:tipo git :repo …)` axis routes its `:repo` \
5285 value through via `DepSource::validate`; the canonical authoring \
5286 shapes are `\"github:org/repo\"` shorthand, `\"https://host/path\"` \
5287 / `\"ssh://[user@]host/path\"` / `\"git://host/path\"` / \
5288 `\"file:///path\"` URL schemes, or the `\"git@host:path\"` \
5289 scp-style SSH form. Without this gate a malformed `:repositorio` \
5290 (whitespace from a paste-from-doc; control characters / CRLF \
5291 from a paste-from-multiline-doc; a leading `-` from a \
5292 CLI-argument-injection footgun; a missing `:` separator from a \
5293 bare `org/repo` shape git treats as a relative filesystem path) \
5294 silently landed in the rendered `Chart.yaml home:` and the \
5295 FluxCD `GitRepository.spec.url` and broke at `git clone` / \
5296 FluxCD reconcile time far from the source caixa.lisp; the gate \
5297 moves the diagnostic to the manifest layer with the offending \
5298 value named verbatim)"
5299 )]
5300 RepositorioInvalid { repositorio: String, reason: String },
5301 #[error(
5302 ":descricao is the empty string (every published caixa names \
5303 its purpose via a non-empty `:descricao` summary — the value \
5304 flows verbatim into the rendered `lareira-<nome>` Helm \
5305 chart's `Chart.yaml` `description:` field via `caixa-helm`'s \
5306 `build_chart_yaml` and into the chart `README.md` header via \
5307 `build_readme`; both consumers' `Option::unwrap_or_else` \
5308 `caixa.nome`-derived fallbacks only fire when the slot is \
5309 `None`, so an empty `Some(\"\")` silently lands as \
5310 `description: \"\"` / a blank `README.md` header in the \
5311 rendered artifacts and breaks at `helm lint` time \
5312 (`WARNING [chart.metadata.description]: description is \
5313 required` on `apiVersion: v2` charts) far from the source \
5314 caixa.lisp; omit the slot entirely to defer to the \
5315 renderer's `\"Generated chart for caixa Servico <nome>\"` / \
5316 `\"caixa Servico <nome>\"` fallbacks, or carry a non-empty \
5317 summary like `\"Canonical Rust→wasm32-wasip2 caixa \
5318 Servico.\"`)"
5319 )]
5320 DescricaoEmpty,
5321 #[error(
5322 ":descricao {descricao:?} is not a valid chart-description shape: \
5323 {reason} (the substrate consumes this string through the shared \
5324 `crate::render::is_chart_description_shape` predicate — the same \
5325 single-line-UTF-8 floor every realistic chart description carries: \
5326 1..=512 bytes, no leading or trailing whitespace, no ASCII control \
5327 characters anywhere, Unicode prose bytes accepted. The canonical \
5328 authoring shapes are short single-line summaries like `\"Canonical \
5329 Rust→wasm32-wasip2 caixa Servico.\"`, `\"Checkout flow.\"`, \
5330 `\"AWS provider caixa for tatara-lisp\"`. Without this gate a \
5331 malformed `:descricao` (paste-from-aligned-doc leading whitespace \
5332 `\" Checkout flow.\"` / trailing whitespace `\"Checkout flow. \"`; \
5333 paste-from-multiline-doc newline `\"Checkout\\nflow.\"`; \
5334 paste-from-Windows-CRLF-doc carriage return `\"Checkout\\rflow.\"`; \
5335 tab-from-aligned-doc `\"Checkout\\tflow.\"`; paste-from-binary-blob \
5336 NUL / BEL / ESC / DEL byte) silently passed `from_lisp` + \
5337 `validate_descricao` + `StandardLayout::verify` and landed in the \
5338 rendered `lareira-<nome>` Helm chart's `Chart.yaml description:` \
5339 field + `README.md` header paragraph as a YAML-illegal multi-line \
5340 scalar or a silently-trimmed whitespace round-trip — every \
5341 chart-aware UI (`helm list`, `helm search`, Artifact Hub) would \
5342 render the description in a single-line column far from the source \
5343 caixa.lisp; the gate moves the diagnostic to the manifest layer \
5344 with the offending value named verbatim)"
5345 )]
5346 DescricaoInvalid { descricao: String, reason: String },
5347 #[error(
5348 ":licenca is the empty string (every published caixa names \
5349 its license via a non-empty `:licenca` SPDX expression — the \
5350 value flows verbatim into the rendered `lareira-<nome>` Helm \
5351 chart's `README.md` `## License` section via `caixa-helm`'s \
5352 `build_readme` at `caixa-helm/src/lib.rs:361`; the consumer's \
5353 `Option::unwrap_or_else(|| \"MIT\".into())` `MIT` fallback \
5354 only fires when the slot is `None`, so an empty `Some(\"\")` \
5355 silently lands as a bare trailing period in the rendered \
5356 chart `README.md` `License` section far from the source \
5357 caixa.lisp; omit the slot entirely to defer to the \
5358 renderer's `MIT` fallback, or carry a canonical SPDX \
5359 expression like `\"MIT\"`, `\"Apache-2.0\"`, \
5360 `\"Apache-2.0 OR MIT\"`)"
5361 )]
5362 LicencaEmpty,
5363 #[error(
5364 ":licenca {licenca:?} is not a valid SPDX expression shape: {reason} \
5365 (the substrate consumes this string through the shared \
5366 `crate::render::is_spdx_expression_shape` predicate — the same \
5367 alphabet-floor parser every peer per-axis value-shape gate routes \
5368 its value through; the canonical authoring shapes are single \
5369 license identifiers like `\"MIT\"`, `\"Apache-2.0\"`, `\"BSD-3-Clause\"`, \
5370 compound expressions like `\"Apache-2.0 OR MIT\"`, \
5371 `\"MIT AND BSD-3-Clause\"`, `\"(MIT OR Apache-2.0) AND ISC\"`, \
5372 license-with-exception forms like `\"Apache-2.0 WITH LLVM-exception\"`, \
5373 `+`-suffix variants like `\"GPL-2.0+\"`, and user-defined references \
5374 like `\"LicenseRef-MyLicense\"` / \
5375 `\"DocumentRef-doc:LicenseRef-MyLicense\"`. Without this gate a \
5376 malformed `:licenca` (paste-from-doc whitespace `\"MIT \"` / \
5377 `\" MIT\"`; paste-from-multiline-doc CRLF `\"MIT\\n\"`; \
5378 tab-from-aligned-doc `\"MIT\\tOR Apache-2.0\"`; non-ASCII byte from \
5379 a smart-quote paste; underscore-instead-of-hyphen typo \
5380 `\"Apache_2.0\"`; comma-instead-of-`OR`-keyword colloquial idiom \
5381 `\"MIT, Apache-2.0\"`; slash-dual-license colloquial idiom \
5382 `\"MIT/Apache-2.0\"`; semicolon-list-separator confusion \
5383 `\"MIT; Apache-2.0\"`) silently landed in the rendered chart \
5384 `README.md` `## License` section + a future SPDX-aware \
5385 `Chart.yaml license:` emitter would refuse the value at \
5386 `helm lint` time far from the source caixa.lisp; the gate moves \
5387 the diagnostic to the manifest layer with the offending value \
5388 named verbatim)"
5389 )]
5390 LicencaInvalid { licenca: String, reason: String },
5391 #[error(
5392 ":edicao is the empty string (every published caixa names \
5393 its language edition via a non-empty `:edicao` value — the \
5394 edition determines the tatara-lisp macro surface + \
5395 compatibility flags the substrate applies when building \
5396 the caixa; the canonical `Caixa::template` scaffold every \
5397 `feira init` emits carries `:edicao \"2026\"` verbatim and \
5398 every renderer-side fixture (`caixa-helm`, `caixa-flux`, \
5399 `caixa-mesh`) carries `edicao: Some(\"2026\".into())` by \
5400 construction, so an empty `Some(\"\")` silently lands as a \
5401 bare `(:edicao \"\")` line in the rendered `caixa.lisp` and \
5402 a future renderer-side consumer that folds it through \
5403 `Option::unwrap_or_else` will skip the fallback and pass the \
5404 empty edition through to the substrate's build-time edition \
5405 selector far from the source caixa.lisp; omit the slot \
5406 entirely to defer to the substrate's default edition, or \
5407 carry a canonical edition like `\"2026\"`)"
5408 )]
5409 EdicaoEmpty,
5410 #[error(
5411 ":edicao {edicao:?} is not a valid edition: {reason} (every \
5412 documented tatara-lisp edition is a 4-digit ASCII decimal \
5413 year — `\"2026\"` is the only edition currently minted; \
5414 future-introduced siblings will follow the same shape, peer \
5415 with Cargo's `[package] edition` grammar which every value \
5416 Cargo has ever accepted matches: `\"2015\"`, `\"2018\"`, \
5417 `\"2021\"`, `\"2024\"`. Without this gate the canonical \
5418 paste-from-doc footguns silently passed: a trailing space \
5419 (`\"2026 \"`) from a paste-from-doc, a CRLF (`\"2026\\n\"`) \
5420 from a paste-from-multiline-doc, a fullwidth-keyboard \
5421 look-alike (`\"2026\"`), a free-form non-year value \
5422 (`\"x\"`, `\"latest\"`, `\"nightly\"`), a leading non-digit \
5423 version-tag prefix (`\"v2026\"`, `\"e2026\"`), a \
5424 decimal-shaped pseudo-version (`\"2026.1\"`), or a \
5425 wrong-length numeric value (`\"26\"`, `\"202\"`, \
5426 `\"20260\"`) all landed as `(:edicao \"<garbage>\")` in the \
5427 rendered caixa.lisp and broke at the substrate's \
5428 build-time edition selector far from the source caixa.lisp; \
5429 omit the slot entirely to defer to the substrate's default \
5430 edition, or carry a canonical 4-digit ASCII decimal year \
5431 like `\"2026\"`)"
5432 )]
5433 EdicaoInvalid { edicao: String, reason: String },
5434}
5435
5436#[cfg(test)]
5437mod tests {
5438 use super::*;
5439
5440 #[test]
5441 fn template_round_trips() {
5442 let src = Caixa::template("demo");
5443 let c = Caixa::from_lisp(&src).expect("template must parse");
5444 assert_eq!(c.nome, "demo");
5445 assert_eq!(c.versao, "0.1.0");
5446 assert_eq!(c.kind, CaixaKind::Biblioteca);
5447 assert_eq!(c.bibliotecas, vec!["lib/demo.lisp".to_string()]);
5448 assert!(c.deps.is_empty());
5449 assert!(c.deps_dev.is_empty());
5450 }
5451
5452 #[test]
5453 fn register_populates_registry() {
5454 Caixa::register().expect("first register call in this test process must succeed");
5455 let kws = tatara_lisp::domain::registered_keywords();
5456 assert!(kws.contains(&"defcaixa"));
5457 }
5458
5459 #[test]
5460 fn to_lisp_round_trips() {
5461 let src = Caixa::template("demo");
5462 let c1 = Caixa::from_lisp(&src).unwrap();
5463 let emitted = c1.to_lisp();
5464 let c2 = Caixa::from_lisp(&emitted).expect("emitted lisp parses back");
5465 assert_eq!(c1, c2);
5466 }
5467
5468 // ── DialetoEstrangeiro carries a single typed axis ────────────────────
5469 //
5470 // The compounding pin: the variant stores only the typed
5471 // [`crate::dialeto::CaixaDialeto`], and every user-facing byte-string
5472 // (canonical keyword, description, consumer) routes through the enum's
5473 // own accessors at Display time. Prior to that closure the variant
5474 // carried each accessor's return value as a stored `&'static str`
5475 // snapshot alongside `dialeto`; a caller could construct the variant
5476 // with a snapshot that drifted from what `dialeto`'s accessors would
5477 // return, and every downstream user-facing projection would silently
5478 // disagree with the classification. Storing only the axis makes the
5479 // drift structurally impossible.
5480
5481 #[test]
5482 fn dialeto_estrangeiro_variant_carries_only_the_typed_dialeto_axis() {
5483 // Single-field construction is the whole compounding shape — a
5484 // future re-introduction of a snapshot field (a `palavra_canonica:
5485 // &'static str`, a stored `descricao:`, a stored `consumidor:`)
5486 // would re-open the drift surface and this construction would fail
5487 // to compile with "missing field" until every snapshot was seeded
5488 // at the call site again. The compile-time guarantee is the
5489 // invariant; the assertion below only witnesses that the
5490 // construction is well-formed after the closure.
5491 let err = LeituraError::DialetoEstrangeiro {
5492 dialeto: crate::dialeto::CaixaDialeto::Molde,
5493 };
5494 assert!(matches!(
5495 err,
5496 LeituraError::DialetoEstrangeiro {
5497 dialeto: crate::dialeto::CaixaDialeto::Molde,
5498 }
5499 ));
5500 }
5501
5502 #[test]
5503 fn dialeto_estrangeiro_display_routes_through_typed_dialeto_accessors() {
5504 // For every foreign-dialect classification the variant surfaces —
5505 // [`crate::dialeto::CaixaDialeto::Molde`] and
5506 // [`crate::dialeto::CaixaDialeto::MoldePosicional`], the two
5507 // variants [`Caixa::from_lisp`] raises this error for — the
5508 // rendered [`std::fmt::Display`] byte-string must interpolate each
5509 // typed accessor's return verbatim. A future re-introduction of a
5510 // stored `&'static str` snapshot alongside `dialeto` that Display
5511 // read instead of the accessor would fail this pin as soon as the
5512 // two disagreed; a future accessor rebrand (a per-dialect
5513 // consumer rename, a canonical-keyword shift once the substrate
5514 // migration named in [`crate::dialeto`] completes) reaches every
5515 // consumer through one typed dispatch and this pin verifies the
5516 // display path is one of them.
5517 for d in [
5518 crate::dialeto::CaixaDialeto::Molde,
5519 crate::dialeto::CaixaDialeto::MoldePosicional,
5520 ] {
5521 let rendered = LeituraError::DialetoEstrangeiro { dialeto: d }.to_string();
5522 assert!(
5523 rendered.contains(d.palavra_canonica()),
5524 "Display must interpolate `dialeto.palavra_canonica()` \
5525 verbatim — a stored snapshot would silently drift from \
5526 the typed accessor. dialect: {d}, rendered: {rendered:?}"
5527 );
5528 assert!(
5529 rendered.contains(d.descricao()),
5530 "Display must interpolate `dialeto.descricao()` verbatim. \
5531 dialect: {d}, rendered: {rendered:?}"
5532 );
5533 assert!(
5534 rendered.contains(d.consumidor()),
5535 "Display must interpolate `dialeto.consumidor()` verbatim. \
5536 dialect: {d}, rendered: {rendered:?}"
5537 );
5538 }
5539 }
5540
5541 #[test]
5542 fn from_lisp_rejects_molde_dialect_via_typed_variant() {
5543 // The end-to-end pin the compounding closure defends: a
5544 // Molde-dialect source lands as [`LeituraError::DialetoEstrangeiro`]
5545 // carrying [`crate::dialeto::CaixaDialeto::Molde`], and the
5546 // rendered Display byte-string names the Molde accessors'
5547 // returns verbatim. Any future path that constructed the variant
5548 // with a mismatched snapshot (a stored `palavra_canonica:
5549 // "defcaixa"` on a `Molde` classification) would land Display
5550 // pointing at `defcaixa` while the typed axis said `Molde` — the
5551 // exact drift the closure removes.
5552 let src = r#"
5553 (defcaixa
5554 :name "x"
5555 :kind :Biblioteca
5556 :ecosystem :rust-single-crate
5557 :package {:name "x" :version "0.1.0"})
5558 "#;
5559 let err = Caixa::from_lisp(src).expect_err("Molde dialect must not parse as Pacote");
5560 match err {
5561 LeituraError::DialetoEstrangeiro { dialeto } => {
5562 assert_eq!(dialeto, crate::dialeto::CaixaDialeto::Molde);
5563 let rendered = LeituraError::DialetoEstrangeiro { dialeto }.to_string();
5564 assert!(rendered.contains(dialeto.palavra_canonica()));
5565 assert!(rendered.contains(dialeto.consumidor()));
5566 assert!(rendered.contains(dialeto.descricao()));
5567 }
5568 other => panic!("expected DialetoEstrangeiro, got {other:?}"),
5569 }
5570 }
5571
5572 #[test]
5573 fn from_lisp_rejects_molde_posicional_dialect_via_typed_variant() {
5574 // Coverage pin for the [`crate::dialeto::CaixaDialeto::MoldePosicional`]
5575 // arm of the [`Caixa::from_lisp`] foreign-dialect gate — the
5576 // positional-arity `defmolde` form written under a `(defcaixa …)`
5577 // head (`(defcaixa todoku-go :kind :Biblioteca :ecosystem :go
5578 // …)`). Pre-lift this arm rode the same `foreign =>` wildcard
5579 // the [`crate::dialeto::CaixaDialeto::Molde`] sibling arm rode,
5580 // so no test exercised the positional-arity path through
5581 // `Caixa::from_lisp` specifically; the sibling
5582 // [`from_lisp_rejects_molde_dialect_via_typed_variant`] only
5583 // covered [`crate::dialeto::CaixaDialeto::Molde`]. Post-lift the
5584 // two arms route through the lifted
5585 // [`crate::dialeto::CaixaDialeto::is_molde_family`] (e9d2315)
5586 // typed predicate — the same predicate the pre-lift `foreign =>`
5587 // wildcard resolved to today — and this pin makes the
5588 // positional-arity arm's byte-shape at the gate explicit rather
5589 // than implied by wildcard-absorption. A future regression that
5590 // silently reordered [`crate::dialeto::CaixaDialeto::is_molde_family`]'s
5591 // arm-set (dropped [`crate::dialeto::CaixaDialeto::MoldePosicional`]
5592 // from the two-arity closure) would fail this pin at caixa-core
5593 // test time rather than surfacing far from the change as a
5594 // `caixa.lisp` carrying a `(defcaixa todoku-go :ecosystem :go
5595 // …)` silently parsing past the derive.
5596 let src = r#"
5597 (defcaixa todoku-go
5598 :kind :Biblioteca
5599 :ecosystem :go
5600 :package {:name "todoku-go" :version "0.3.0"})
5601 "#;
5602 let err =
5603 Caixa::from_lisp(src).expect_err("MoldePosicional dialect must not parse as Pacote");
5604 match err {
5605 LeituraError::DialetoEstrangeiro { dialeto } => {
5606 assert_eq!(
5607 dialeto,
5608 crate::dialeto::CaixaDialeto::MoldePosicional,
5609 "DialetoEstrangeiro must carry the MoldePosicional \
5610 variant verbatim — the positional-arity `defmolde` \
5611 form under a `(defcaixa …)` head is the \
5612 `MoldePosicional` arm's canonical byte-shape"
5613 );
5614 let rendered = LeituraError::DialetoEstrangeiro { dialeto }.to_string();
5615 assert!(
5616 rendered.contains(dialeto.palavra_canonica()),
5617 "Display must interpolate `dialeto.palavra_canonica()` \
5618 verbatim on the MoldePosicional arm; rendered: \
5619 {rendered:?}"
5620 );
5621 assert!(
5622 rendered.contains(dialeto.consumidor()),
5623 "Display must interpolate `dialeto.consumidor()` \
5624 verbatim on the MoldePosicional arm; rendered: \
5625 {rendered:?}"
5626 );
5627 assert!(
5628 rendered.contains(dialeto.descricao()),
5629 "Display must interpolate `dialeto.descricao()` \
5630 verbatim on the MoldePosicional arm; rendered: \
5631 {rendered:?}"
5632 );
5633 }
5634 other => panic!("expected DialetoEstrangeiro, got {other:?}"),
5635 }
5636 }
5637
5638 #[test]
5639 fn from_lisp_dialect_gate_dispatches_through_caixa_dialeto_is_molde_family_predicate() {
5640 // Load-bearing byte-parity pin: for every arm in
5641 // [`crate::dialeto::CaixaDialeto::ALL`], the
5642 // [`Caixa::from_lisp`] foreign-dialect gate's DialetoEstrangeiro
5643 // partition must agree with the lifted
5644 // [`crate::dialeto::CaixaDialeto::is_molde_family`] (e9d2315)
5645 // typed predicate — i.e. from_lisp raises
5646 // [`LeituraError::DialetoEstrangeiro`] carrying `d` iff
5647 // `d.is_molde_family()` returns `true`, and does NOT raise
5648 // [`LeituraError::DialetoEstrangeiro`] on any arm where the
5649 // predicate returns `false` (the arm's source falls through to
5650 // the derive — parses cleanly on
5651 // [`crate::dialeto::CaixaDialeto::Pacote`], surfaces a
5652 // [`LeituraError::Leitura`] on
5653 // [`crate::dialeto::CaixaDialeto::Desconhecido`]).
5654 //
5655 // Pre-lift the gate hand-rolled a three-arm match
5656 // (`Pacote => {}`, `Desconhecido => {}`, `foreign => Err(…)`)
5657 // whose `foreign =>` wildcard expressed no compile-time link
5658 // back to the substrate primitive's arm-family; a future fifth
5659 // dialect the [`crate::dialeto`] module doc's "third dialect"
5660 // hazard actualises would fall silently onto the wildcard
5661 // regardless of whether it belonged to the `defmolde` family or
5662 // to a distinct `defcaixa`-family. Post-lift the partition
5663 // resolves through
5664 // [`crate::dialeto::CaixaDialeto::is_molde_family`]'s single
5665 // typed dispatch, and this pin refuses any future regression
5666 // that silently split the from_lisp partition from the typed
5667 // predicate — the two paths now migrate as one on any future
5668 // arm addition.
5669 //
5670 // Sibling in shape to the peer
5671 // [`crate::dialeto::tests::caixa_dialeto_is_molde_family_agrees_with_palavra_canonica_defmolde_projection`]
5672 // (e9d2315) that pins the same byte-parity between
5673 // [`crate::dialeto::CaixaDialeto::is_molde_family`] and the
5674 // sibling [`crate::dialeto::CaixaDialeto::palavra_canonica`]
5675 // `== "defmolde"` classifier — extends the discipline from the
5676 // two paths within the [`crate::dialeto`] primitive onto the
5677 // third external consumer of the `defmolde`-family partition
5678 // (the [`Caixa::from_lisp`] gate that raises
5679 // [`LeituraError::DialetoEstrangeiro`]).
5680 let fixtures: &[(crate::dialeto::CaixaDialeto, &str)] = &[
5681 (
5682 crate::dialeto::CaixaDialeto::Pacote,
5683 r#"
5684 (defcaixa
5685 :nome "checkout"
5686 :versao "0.1.0"
5687 :kind Biblioteca
5688 :edicao "2026"
5689 :descricao "canonical Pacote source"
5690 :autores ()
5691 :etiquetas ()
5692 :deps ()
5693 :deps-dev ()
5694 :bibliotecas ("lib/checkout.lisp"))
5695 "#,
5696 ),
5697 (
5698 crate::dialeto::CaixaDialeto::Molde,
5699 r#"
5700 (defcaixa
5701 :name "base64"
5702 :kind :Biblioteca
5703 :ecosystem :rust-single-crate
5704 :package {:name "base64" :version "0.22.1"}
5705 :workflows [:auto-release])
5706 "#,
5707 ),
5708 (
5709 crate::dialeto::CaixaDialeto::MoldePosicional,
5710 r#"
5711 (defcaixa todoku-go
5712 :kind :Biblioteca
5713 :ecosystem :go
5714 :package {:name "todoku-go" :version "0.3.0"})
5715 "#,
5716 ),
5717 (
5718 crate::dialeto::CaixaDialeto::Desconhecido,
5719 r#"(defcaixa :licenca "MIT")"#,
5720 ),
5721 ];
5722
5723 // Coverage: every arm in [`crate::dialeto::CaixaDialeto::ALL`]
5724 // must appear in the fixture table so the pin's arm-set stays
5725 // synchronised with the enum's arm-set. Fails at test time if a
5726 // future fifth arm added to [`crate::dialeto::CaixaDialeto`]
5727 // (with a corresponding `is_molde_family` return) forgot to
5728 // extend this fixture table with a canonical source for the new
5729 // arm — the pin cannot cover an arm it has no source for.
5730 for &expected in crate::dialeto::CaixaDialeto::ALL {
5731 assert!(
5732 fixtures.iter().any(|(d, _)| *d == expected),
5733 "fixture table must carry a canonical source for every \
5734 CaixaDialeto arm; missing: {expected:?}"
5735 );
5736 }
5737
5738 for &(expected_dialect, src) in fixtures {
5739 let classified = crate::dialeto::classify(src.trim()).unwrap_or_else(|err| {
5740 panic!(
5741 "fixture source for {expected_dialect:?} must classify \
5742 cleanly, got err: {err:?}"
5743 )
5744 });
5745 assert_eq!(
5746 classified, expected_dialect,
5747 "fixture source for {expected_dialect:?} must classify as \
5748 {expected_dialect:?} (drift here defeats the byte-parity \
5749 pin below — a source labelled for one arm but classifying \
5750 as another would silently satisfy or violate the pin for \
5751 the wrong reason)"
5752 );
5753
5754 let outcome = Caixa::from_lisp(src);
5755 match (expected_dialect.is_molde_family(), &outcome) {
5756 (true, Err(LeituraError::DialetoEstrangeiro { dialeto })) => {
5757 assert_eq!(
5758 *dialeto, expected_dialect,
5759 "DialetoEstrangeiro must carry the same typed arm \
5760 the classifier returned — a drift here would let \
5761 from_lisp raise the error while pointing at the \
5762 wrong dialect (e.g. rejecting a \
5763 MoldePosicional source as Molde). arm: \
5764 {expected_dialect:?}"
5765 );
5766 }
5767 (true, other) => panic!(
5768 "arm {expected_dialect:?} has is_molde_family() = true \
5769 so from_lisp must raise DialetoEstrangeiro carrying \
5770 {expected_dialect:?}; got: {other:?}"
5771 ),
5772 (false, Err(LeituraError::DialetoEstrangeiro { dialeto })) => panic!(
5773 "arm {expected_dialect:?} has is_molde_family() = false \
5774 so from_lisp must NOT raise DialetoEstrangeiro; got \
5775 one carrying: {dialeto:?}. This means the typed \
5776 predicate and the from_lisp partition disagree on \
5777 this arm — exactly the drift this pin refuses."
5778 ),
5779 (false, _) => {
5780 // A non-molde arm's source falls through to the
5781 // derive: Pacote sources parse to Ok(_); Desconhecido
5782 // sources surface as LeituraError::Leitura from the
5783 // derive's own unknown-keyword rejection. Either
5784 // shape is acceptable here — the pin's promise is
5785 // narrower: "no DialetoEstrangeiro on
5786 // is_molde_family() == false".
5787 }
5788 }
5789 }
5790 }
5791
5792 // ── M2 typed-substrate slot tests (limits, behavior, upgrade-from, supervisor) ──
5793
5794 #[test]
5795 fn limits_round_trip_via_json() {
5796 use crate::LimitsSpec;
5797 use std::time::Duration;
5798 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5799 c.limits = Some(LimitsSpec {
5800 memory: Some(64 * 1024 * 1024),
5801 fuel: Some(1_000_000),
5802 wall_clock: Some(Duration::from_secs(30)),
5803 cpu: Some(500),
5804 });
5805 let json = serde_json::to_string(&c).unwrap();
5806 assert!(json.contains("\"limits\""));
5807 assert!(json.contains("\"64MiB\""));
5808 assert!(json.contains("\"30s\""));
5809 assert!(json.contains("\"500m\""));
5810 let back: Caixa = serde_json::from_str(&json).unwrap();
5811 assert_eq!(c.limits, back.limits);
5812 }
5813
5814 #[test]
5815 fn behavior_round_trip_via_json() {
5816 use crate::BehaviorSpec;
5817 use std::path::PathBuf;
5818 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5819 c.behavior = Some(BehaviorSpec {
5820 on_init: Some(PathBuf::from("lib/init.lisp")),
5821 on_call: Some(PathBuf::from("lib/handlers.lisp")),
5822 ..Default::default()
5823 });
5824 let json = serde_json::to_string(&c).unwrap();
5825 let back: Caixa = serde_json::from_str(&json).unwrap();
5826 assert_eq!(c.behavior, back.behavior);
5827 }
5828
5829 #[test]
5830 fn upgrade_from_round_trip_via_json() {
5831 use crate::{UpgradeFromEntry, UpgradeInstruction};
5832 use std::path::PathBuf;
5833 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5834 c.upgrade_from = vec![UpgradeFromEntry {
5835 from: "0.1.0".into(),
5836 instructions: vec![
5837 UpgradeInstruction::LoadModule {
5838 module: "demo".into(),
5839 },
5840 UpgradeInstruction::StateChange {
5841 script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
5842 },
5843 UpgradeInstruction::SoftPurge {
5844 module: "demo-old".into(),
5845 },
5846 ],
5847 }];
5848 let json = serde_json::to_string(&c).unwrap();
5849 let back: Caixa = serde_json::from_str(&json).unwrap();
5850 assert_eq!(c.upgrade_from, back.upgrade_from);
5851 }
5852
5853 #[test]
5854 fn supervisor_view_returns_typed_shape() {
5855 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
5856 let mut c = Caixa::from_lisp(&Caixa::template("root")).unwrap();
5857 c.kind = CaixaKind::Supervisor;
5858 c.bibliotecas.clear();
5859 c.estrategia = Some(RestartStrategy::OneForOne);
5860 c.max_restarts = Some(5);
5861 c.restart_window = Some("60s".into());
5862 c.children = vec![ChildSpec {
5863 caixa: "worker".into(),
5864 versao: "^0.1".into(),
5865 restart: RestartPolicy::Permanent,
5866 }];
5867 let view = c.supervisor_view().expect("Supervisor kind has a view");
5868 assert_eq!(view.estrategia, RestartStrategy::OneForOne);
5869 assert_eq!(view.max_restarts, 5);
5870 assert_eq!(
5871 view.restart_window,
5872 Some(std::time::Duration::from_secs(60))
5873 );
5874 assert_eq!(view.children.len(), 1);
5875 view.validate().unwrap();
5876 }
5877
5878 #[test]
5879 fn supervisor_view_none_for_non_supervisor_kinds() {
5880 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5881 assert!(c.supervisor_view().is_none());
5882 }
5883
5884 #[test]
5885 fn declared_mesh_slots_empty_for_bare_caixa() {
5886 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5887 assert!(c.declared_mesh_slots().is_empty());
5888 }
5889
5890 #[test]
5891 fn declared_mesh_slots_reports_only_set_slots_in_canonical_order() {
5892 use crate::{Entrada, Membro};
5893 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5894 // Set a non-adjacent pair (:membros + :entrada) to pin that the
5895 // canonical declaration order is preserved regardless of which
5896 // subset is populated.
5897 c.membros = vec![Membro {
5898 caixa: "a".into(),
5899 versao: "^0.1".into(),
5900 }];
5901 c.entrada = Some(Entrada {
5902 host: "x.example.com".into(),
5903 para: "a".into(),
5904 paths: vec![],
5905 port: 8080,
5906 });
5907 assert_eq!(
5908 c.declared_mesh_slots(),
5909 vec![
5910 crate::render::M3_AUTHOR_KEY_MEMBROS,
5911 crate::render::M3_AUTHOR_KEY_ENTRADA,
5912 ]
5913 );
5914 }
5915
5916 #[test]
5917 fn m3_top_level_author_key_consts_pin_canonical_kebab_case_labels() {
5918 // Scalar-value pin: the five author-facing kebab-case labels the
5919 // `(defcaixa … :<slot> (…))` surface admits on the M3 top-level
5920 // mesh slot axis, one arm per typed slot. Mirrors the peer
5921 // scalar-value pin the sibling
5922 // [`crate::M2_AUTHOR_KEY_LIMITS`] /
5923 // [`crate::M2_AUTHOR_KEY_BEHAVIOR`] /
5924 // [`crate::M2_AUTHOR_KEY_UPGRADE_FROM`] M2 top-level slot consts
5925 // carry (f49c8b0), so both altitudes of the typed-slot algebra
5926 // (per-Servico M2 + per-Aplicacao M3) share the same
5927 // "one canonical byte-string per arm" discipline. A future
5928 // rebrand (`:membros` → `:members`, `:contratos` → `:contracts`,
5929 // `:politicas` → `:policies`, `:placement` → `:distribution`,
5930 // `:entrada` → `:ingress`) lands as an edit to exactly one const,
5931 // and every consumer that reaches for the label picks it up at
5932 // build time rather than at runtime as a downstream mismatch.
5933 assert_eq!(crate::render::M3_AUTHOR_KEY_MEMBROS, ":membros");
5934 assert_eq!(crate::render::M3_AUTHOR_KEY_CONTRATOS, ":contratos");
5935 assert_eq!(crate::render::M3_AUTHOR_KEY_POLITICAS, ":politicas");
5936 assert_eq!(crate::render::M3_AUTHOR_KEY_PLACEMENT, ":placement");
5937 assert_eq!(crate::render::M3_AUTHOR_KEY_ENTRADA, ":entrada");
5938 }
5939
5940 #[test]
5941 fn declared_mesh_slots_route_through_lifted_m3_author_key_consts() {
5942 // Production-through-const pin: the five per-arm labels the
5943 // [`Caixa::declared_mesh_slots`] tagger pushes onto its return
5944 // `Vec` route through the lifted
5945 // [`crate::M3_AUTHOR_KEY_MEMBROS`] /
5946 // [`crate::M3_AUTHOR_KEY_CONTRATOS`] /
5947 // [`crate::M3_AUTHOR_KEY_POLITICAS`] /
5948 // [`crate::M3_AUTHOR_KEY_PLACEMENT`] /
5949 // [`crate::M3_AUTHOR_KEY_ENTRADA`] consts, in canonical
5950 // declaration order. A future re-order or drift at the tagger
5951 // (a rename that reaches the tagger but not the const, or vice
5952 // versa) surfaces here at build time rather than at runtime as
5953 // a [`crate::LayoutError::MeshSlotsOnNonAplicacao`]
5954 // `slots: <stale-kebab-case>` diagnostic far from the rename's
5955 // commit. Mirror of the peer
5956 // [`declared_servico_slots_route_through_lifted_m2_author_key_consts`]
5957 // pin (f49c8b0) on the sibling per-Servico M2 top-level slot
5958 // axis.
5959 use crate::{Entrada, Membro, MeshPolicy, Placement, PlacementStrategy, WitContract};
5960 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5961 c.membros = vec![Membro {
5962 caixa: "a".into(),
5963 versao: "^0.1".into(),
5964 }];
5965 c.contratos = vec![WitContract {
5966 de: "a".into(),
5967 para: "a".into(),
5968 wit: "wasi:http/proxy".into(),
5969 endpoint: Some("/x".into()),
5970 subject: None,
5971 slot: None,
5972 }];
5973 c.politicas = Some(MeshPolicy::default());
5974 c.placement = Some(Placement {
5975 estrategia: PlacementStrategy::Replicated,
5976 clusters: vec!["rio".into()],
5977 affinity: None,
5978 shard_key: None,
5979 });
5980 c.entrada = Some(Entrada {
5981 host: "x.example.com".into(),
5982 para: "a".into(),
5983 paths: vec![],
5984 port: 8080,
5985 });
5986 assert_eq!(
5987 c.declared_mesh_slots(),
5988 vec![
5989 crate::render::M3_AUTHOR_KEY_MEMBROS,
5990 crate::render::M3_AUTHOR_KEY_CONTRATOS,
5991 crate::render::M3_AUTHOR_KEY_POLITICAS,
5992 crate::render::M3_AUTHOR_KEY_PLACEMENT,
5993 crate::render::M3_AUTHOR_KEY_ENTRADA,
5994 ]
5995 );
5996 }
5997
5998 #[test]
5999 fn declared_supervisor_slots_empty_for_bare_caixa() {
6000 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6001 assert!(c.declared_supervisor_slots().is_empty());
6002 }
6003
6004 #[test]
6005 fn declared_supervisor_slots_reports_only_set_slots_in_canonical_order() {
6006 use crate::RestartStrategy;
6007 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6008 // Set a non-adjacent pair (:estrategia + :restart-window) to pin
6009 // that the canonical declaration order is preserved regardless
6010 // of which subset is populated.
6011 c.estrategia = Some(RestartStrategy::OneForOne);
6012 c.restart_window = Some("60s".into());
6013 assert_eq!(
6014 c.declared_supervisor_slots(),
6015 vec![
6016 crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA,
6017 crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW,
6018 ]
6019 );
6020 }
6021
6022 #[test]
6023 fn supervisor_top_level_author_key_consts_pin_canonical_kebab_case_labels() {
6024 // Scalar-value pin: the four author-facing kebab-case labels the
6025 // `(defcaixa … :<slot> (…))` surface admits on the Supervisor
6026 // supervision-tree slot axis, one arm per typed slot. Mirrors the
6027 // peer scalar-value pins the sibling
6028 // [`crate::render::M2_AUTHOR_KEY_LIMITS`] /
6029 // [`crate::render::M2_AUTHOR_KEY_BEHAVIOR`] /
6030 // [`crate::render::M2_AUTHOR_KEY_UPGRADE_FROM`] top-level M2 slot
6031 // consts and [`crate::render::M3_AUTHOR_KEY_MEMBROS`] etc.
6032 // top-level M3 slot consts carry, so all three kind-scoped
6033 // typed-slot-family author-facing-label axes route through one
6034 // canonical per-arm declaration. A future rebrand
6035 // (`:estrategia` → `:strategy` for English uniformity,
6036 // `:max-restarts` → `:max-intensity` matching Erlang/OTP's
6037 // `MaxIntensity` name, `:restart-window` → `:period` matching
6038 // OTP's `Period` name, `:children` → `:workers` matching Elixir
6039 // idiom) lands as an edit to exactly one const, and every
6040 // consumer that reaches for the label picks it up at build time
6041 // rather than at runtime as a downstream mismatch.
6042 assert_eq!(
6043 crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA,
6044 ":estrategia"
6045 );
6046 assert_eq!(
6047 crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS,
6048 ":max-restarts"
6049 );
6050 assert_eq!(
6051 crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW,
6052 ":restart-window"
6053 );
6054 assert_eq!(crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN, ":children");
6055 }
6056
6057 #[test]
6058 fn declared_supervisor_slots_route_through_lifted_supervisor_author_key_consts() {
6059 // Production-through-const pin: the four per-arm labels the
6060 // [`Caixa::declared_supervisor_slots`] tagger pushes onto its
6061 // return `Vec` route through the lifted
6062 // [`crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA`] /
6063 // [`crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS`] /
6064 // [`crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW`] /
6065 // [`crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN`] consts, in
6066 // canonical declaration order. A future re-order or drift at the
6067 // tagger (a rename that reaches the tagger but not the const, or
6068 // vice versa) surfaces here at build time rather than at runtime
6069 // as a [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
6070 // `slots: <stale-kebab-case>` diagnostic far from the rename's
6071 // commit. Mirror of the peer
6072 // [`declared_servico_slots_route_through_lifted_m2_author_key_consts`]
6073 // (f49c8b0) and
6074 // [`declared_mesh_slots_route_through_lifted_m3_author_key_consts`]
6075 // (882f498) pins on the sibling M2 / M3 top-level slot axes.
6076 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6077 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6078 c.estrategia = Some(RestartStrategy::OneForOne);
6079 c.max_restarts = Some(5);
6080 c.restart_window = Some("60s".into());
6081 c.children = vec![ChildSpec {
6082 caixa: "worker".into(),
6083 versao: "^0.1".into(),
6084 restart: RestartPolicy::Permanent,
6085 }];
6086 assert_eq!(
6087 c.declared_supervisor_slots(),
6088 vec![
6089 crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA,
6090 crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS,
6091 crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW,
6092 crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN,
6093 ]
6094 );
6095 }
6096
6097 #[test]
6098 fn declared_servico_slots_empty_for_bare_caixa() {
6099 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6100 assert!(c.declared_servico_slots().is_empty());
6101 }
6102
6103 #[test]
6104 fn declared_servico_slots_reports_only_set_slots_in_canonical_order() {
6105 use crate::{UpgradeFromEntry, UpgradeInstruction};
6106 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6107 // Set a non-adjacent pair (:limits + :upgrade-from) to pin that
6108 // the canonical declaration order is preserved regardless of
6109 // which subset is populated.
6110 c.limits = Some(crate::LimitsSpec {
6111 fuel: Some(1_000_000),
6112 ..Default::default()
6113 });
6114 c.upgrade_from = vec![UpgradeFromEntry {
6115 from: "0.1.0".into(),
6116 instructions: vec![UpgradeInstruction::Restart],
6117 }];
6118 assert_eq!(
6119 c.declared_servico_slots(),
6120 vec![
6121 crate::render::M2_AUTHOR_KEY_LIMITS,
6122 crate::render::M2_AUTHOR_KEY_UPGRADE_FROM,
6123 ]
6124 );
6125 }
6126
6127 #[test]
6128 fn m2_top_level_author_key_consts_pin_canonical_kebab_case_labels() {
6129 // Scalar-value pin: the three author-facing kebab-case labels
6130 // the `(defcaixa … :<slot> (…))` surface admits on the M2
6131 // top-level slot axis, one arm per typed slot. Mirrors the peer
6132 // scalar-value pin the sibling renderer-side
6133 // [`crate::M2_KEY_LIMITS`] / [`crate::M2_KEY_BEHAVIOR`] /
6134 // [`crate::M2_KEY_UPGRADE_FROM`] camelCase overlay-container
6135 // consts carry, so both halves of the M2 top-level slot dual
6136 // axis (author-facing kebab-case label + renderer-side
6137 // camelCase overlay-container wire key) route through one
6138 // canonical per-arm declaration. A future rebrand
6139 // (`:limits` → `:sandbox` matching Lunatic per-process
6140 // terminology INSPIRATIONS §III.1, `:behavior` → `:gen-server`
6141 // matching Erlang's verbatim name, `:upgrade-from` → `:appup`
6142 // matching Erlang's verbatim appup name) lands as an edit to
6143 // exactly one const, and every consumer that reaches for the
6144 // label picks it up at build time rather than at runtime as a
6145 // downstream mismatch.
6146 assert_eq!(crate::render::M2_AUTHOR_KEY_LIMITS, ":limits");
6147 assert_eq!(crate::render::M2_AUTHOR_KEY_BEHAVIOR, ":behavior");
6148 assert_eq!(crate::render::M2_AUTHOR_KEY_UPGRADE_FROM, ":upgrade-from");
6149 }
6150
6151 #[test]
6152 fn declared_servico_slots_route_through_lifted_m2_author_key_consts() {
6153 // Production-through-const pin: the three per-arm labels the
6154 // [`Caixa::declared_servico_slots`] tagger pushes onto its
6155 // return `Vec` route through the lifted
6156 // [`crate::M2_AUTHOR_KEY_LIMITS`] /
6157 // [`crate::M2_AUTHOR_KEY_BEHAVIOR`] /
6158 // [`crate::M2_AUTHOR_KEY_UPGRADE_FROM`] consts, in canonical
6159 // declaration order. A future re-order or drift at the tagger
6160 // (a rename that reaches the tagger but not the const, or vice
6161 // versa) surfaces here at build time rather than at runtime as
6162 // a [`crate::LayoutError::ServicoSlotsOnNonServico`]
6163 // `slots: <stale-kebab-case>` diagnostic far from the rename's
6164 // commit. Mirror of the peer
6165 // [`crate::behavior::BehaviorSpec::declared_slots`] production
6166 // tagger pin (889dc18) on the sibling per-callback axis.
6167 use crate::{BehaviorSpec, UpgradeFromEntry, UpgradeInstruction};
6168 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6169 c.limits = Some(crate::LimitsSpec {
6170 fuel: Some(1_000_000),
6171 ..Default::default()
6172 });
6173 c.behavior = Some(BehaviorSpec {
6174 on_init: Some(PathBuf::from("lib/init.lisp")),
6175 ..Default::default()
6176 });
6177 c.upgrade_from = vec![UpgradeFromEntry {
6178 from: "0.1.0".into(),
6179 instructions: vec![UpgradeInstruction::Restart],
6180 }];
6181 assert_eq!(
6182 c.declared_servico_slots(),
6183 vec![
6184 crate::render::M2_AUTHOR_KEY_LIMITS,
6185 crate::render::M2_AUTHOR_KEY_BEHAVIOR,
6186 crate::render::M2_AUTHOR_KEY_UPGRADE_FROM,
6187 ]
6188 );
6189 }
6190
6191 #[test]
6192 fn existing_manifests_unaffected_by_new_optional_slots() {
6193 // Regression test: a caixa.lisp authored before M2 typed slots
6194 // should still parse + serialize cleanly. The bare `defcaixa`
6195 // emitted by `Caixa::template` has none of the new fields.
6196 let src = Caixa::template("legacy");
6197 let c = Caixa::from_lisp(&src).unwrap();
6198 assert!(c.limits.is_none());
6199 assert!(c.behavior.is_none());
6200 assert!(c.upgrade_from.is_empty());
6201 assert!(c.estrategia.is_none());
6202 assert!(c.children.is_empty());
6203
6204 // And to_lisp emits a manifest with the new slots in the
6205 // empty/default state — round-trippable.
6206 let emitted = c.to_lisp();
6207 let back = Caixa::from_lisp(&emitted).unwrap();
6208 assert_eq!(c, back);
6209 }
6210
6211 #[test]
6212 fn validate_deps_accepts_canonical_caixa() {
6213 // Positive control: the bare template — zero deps, zero
6214 // deps_dev — passes the gate trivially. A future axis added to
6215 // `Dep::validate` mustn't regress an empty-deps caixa to a
6216 // build error.
6217 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6218 c.validate_deps().unwrap();
6219 }
6220
6221 #[test]
6222 fn validate_deps_rejects_invalid_versao_in_deps() {
6223 // Fail-before-pass-after pin: a malformed `:deps :versao`
6224 // surfaces at validate_deps() time, not at lacre-resolve time.
6225 // Mirrors `rejects_invalid_membro_versao_requirement` and
6226 // `validate_rejects_invalid_child_versao_requirement` on the
6227 // other two `:versao` axes.
6228 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6229 c.deps = vec![Dep::simple("caixa-teia", "^bad-version")];
6230 let err = c.validate_deps().unwrap_err();
6231 assert!(
6232 matches!(
6233 err,
6234 crate::dep::DepError::VersaoInvalid { ref nome, ref versao, .. }
6235 if nome == "caixa-teia" && versao == "^bad-version"
6236 ),
6237 "got {err:?}"
6238 );
6239 }
6240
6241 #[test]
6242 fn validate_deps_rejects_invalid_versao_in_deps_dev() {
6243 // Parity pin: `:deps-dev` must run through the same per-entry
6244 // validator as `:deps` — a typo in either axis surfaces the
6245 // same diagnostic. Without this leg, `:deps-dev` would be a
6246 // second-class citizen of the typed surface and an author
6247 // could land a build that passes validate_deps but fails at
6248 // `feira lock`-time when the dev-dep is resolved for a test
6249 // build.
6250 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6251 c.deps_dev = vec![Dep::simple("tatara-check", "^^0.1")];
6252 let err = c.validate_deps().unwrap_err();
6253 assert!(
6254 matches!(
6255 err,
6256 crate::dep::DepError::VersaoInvalid { ref nome, ref versao, .. }
6257 if nome == "tatara-check" && versao == "^^0.1"
6258 ),
6259 "got {err:?}"
6260 );
6261 }
6262
6263 #[test]
6264 fn validate_deps_runs_deps_before_deps_dev() {
6265 // Order pin: when both lists carry typos, the `:deps`
6266 // diagnostic surfaces first. The author's mental model is
6267 // "runtime deps are load-bearing; dev deps are scaffolding";
6268 // surfacing the runtime axis first matches that hierarchy.
6269 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6270 c.deps = vec![Dep::simple("runtime-dep", "^bad-runtime")];
6271 c.deps_dev = vec![Dep::simple("dev-dep", "^bad-dev")];
6272 let err = c.validate_deps().unwrap_err();
6273 assert!(
6274 matches!(
6275 err,
6276 crate::dep::DepError::VersaoInvalid { ref nome, .. }
6277 if nome == "runtime-dep"
6278 ),
6279 "expected `:deps` typo to surface first, got {err:?}"
6280 );
6281 }
6282
6283 #[test]
6284 fn validate_deps_accepts_canonical_versao_forms_in_both_lists() {
6285 // Positive control sweep across both lists. Pin every
6286 // canonical Cargo-shaped form so a future tightening of the
6287 // accepted set surfaces here as a test failure (parity with
6288 // `accepts_canonical_membro_versao_forms` and
6289 // `validate_accepts_canonical_child_versao_forms`).
6290 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6291 c.deps = vec![
6292 Dep::simple("caret", "^0.1"),
6293 Dep::simple("tilde", "~0.1.2"),
6294 Dep::simple("exact", "0.1.0"),
6295 Dep::simple("wildcard", "*"),
6296 Dep::simple("multi-range", ">=0.1, <2"),
6297 ];
6298 c.deps_dev = vec![
6299 Dep::simple("dev-caret", "^0.1"),
6300 Dep::simple("dev-wildcard", "*"),
6301 ];
6302 c.validate_deps().unwrap();
6303 }
6304
6305 #[test]
6306 fn validate_deps_diagnostic_carries_offending_dep() {
6307 // Diagnostic-shape pin: the error names the offending entry's
6308 // `:nome` + `:versao` verbatim and carries a non-empty
6309 // `reason` from `semver::VersionReq::parse`, so a `feira lint`
6310 // run can render the diagnostic without re-parsing.
6311 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6312 c.deps = vec![Dep::simple("caixa-teia", "not-a-req")];
6313 let err = c.validate_deps().unwrap_err();
6314 let crate::dep::DepError::VersaoInvalid {
6315 nome,
6316 versao,
6317 reason,
6318 } = err
6319 else {
6320 panic!("expected VersaoInvalid, got other variant");
6321 };
6322 assert_eq!(nome, "caixa-teia");
6323 assert_eq!(versao, "not-a-req");
6324 assert!(
6325 !reason.is_empty(),
6326 "VersaoInvalid `reason` must carry the parser's wording verbatim"
6327 );
6328 }
6329
6330 #[test]
6331 fn validate_deps_rejects_ambiguous_fonte_in_deps_dev() {
6332 // Cross-axis pin: `validate_deps` walks both :deps and
6333 // :deps-dev through `Dep::validate`, and the new fonte gate
6334 // (`:tag` + `:branch` both set — the canonical "pin drift"
6335 // footgun) must surface from the :deps-dev arm with the
6336 // offending entry's :nome named. Pin the :deps-dev arm
6337 // explicitly so a future shortcut that only walks :deps
6338 // surfaces here as a regression.
6339 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6340 c.deps_dev = vec![Dep {
6341 nome: "dev-only".into(),
6342 versao: "^0.1".into(),
6343 fonte: Some(crate::DepSource::Git {
6344 repo: "github:p/x".into(),
6345 tag: Some("v1".into()),
6346 rev: None,
6347 branch: Some("main".into()),
6348 }),
6349 opcional: false,
6350 caracteristicas: vec![],
6351 }];
6352 let err = c.validate_deps().unwrap_err();
6353 let crate::dep::DepError::FontePinAmbiguous { nome, pins } = err else {
6354 panic!("expected FontePinAmbiguous from :deps-dev walk");
6355 };
6356 assert_eq!(nome, "dev-only");
6357 assert!(pins.contains(":tag") && pins.contains(":branch"));
6358 }
6359
6360 #[test]
6361 fn validate_deps_rejects_empty_repo_in_deps() {
6362 // Parity pin on the :deps arm: an empty :repo on the runtime
6363 // deps list surfaces the same FonteRepoEmpty diagnostic the
6364 // dep.rs per-entry tests pin, naming the offending entry.
6365 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6366 c.deps = vec![Dep {
6367 nome: "runtime".into(),
6368 versao: "^0.1".into(),
6369 fonte: Some(crate::DepSource::Git {
6370 repo: String::new(),
6371 tag: Some("v1".into()),
6372 rev: None,
6373 branch: None,
6374 }),
6375 opcional: false,
6376 caracteristicas: vec![],
6377 }];
6378 let err = c.validate_deps().unwrap_err();
6379 assert!(
6380 matches!(
6381 err,
6382 crate::dep::DepError::FonteRepoEmpty { ref nome }
6383 if nome == "runtime"
6384 ),
6385 "got {err:?}"
6386 );
6387 }
6388
6389 // ── validate_deps: within-list :nome set-not-multiset gate ─────────
6390
6391 #[test]
6392 fn validate_deps_rejects_duplicate_nome_in_deps() {
6393 // Fail-before-pass-after pin: two `:deps` entries naming the same
6394 // caixa carry two `:versao` / `:fonte` / feature triples that the
6395 // caixa-resolver's lacre pipeline collapses (the second silently
6396 // overwrites the first at `concrete_versao`-resolve time). The
6397 // gate surfaces the duplicate at validate-time, naming the
6398 // offending caixa + the list, before the resolver-side silent
6399 // drop. Mirrors the peer typed-graph duplicate gates
6400 // (`DuplicateChildCaixa`, `MembroDuplicate`, `DuplicateFrom`, …).
6401 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6402 c.deps = vec![
6403 Dep::simple("caixa-teia", "^0.1"),
6404 Dep::simple("caixa-teia", "^0.2"),
6405 ];
6406 let err = c.validate_deps().unwrap_err();
6407 assert!(
6408 matches!(
6409 err,
6410 crate::dep::DepError::DuplicateNome { ref nome, list }
6411 if nome == "caixa-teia" && list == crate::render::DEP_AUTHOR_KEY_DEPS
6412 ),
6413 "got {err:?}"
6414 );
6415 }
6416
6417 #[test]
6418 fn validate_deps_rejects_duplicate_nome_in_deps_dev() {
6419 // Parity pin: `:deps-dev` runs through the same per-list
6420 // duplicate check as `:deps` — neither axis is a second-class
6421 // citizen of the set-not-multiset discipline.
6422 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6423 c.deps_dev = vec![
6424 Dep::simple("tatara-check", "*"),
6425 Dep::simple("tatara-check", "^0.1"),
6426 ];
6427 let err = c.validate_deps().unwrap_err();
6428 assert!(
6429 matches!(
6430 err,
6431 crate::dep::DepError::DuplicateNome { ref nome, list }
6432 if nome == "tatara-check" && list == crate::render::DEP_AUTHOR_KEY_DEPS_DEV
6433 ),
6434 "got {err:?}"
6435 );
6436 }
6437
6438 #[test]
6439 fn validate_deps_accepts_cross_list_same_nome() {
6440 // The Cargo `[dependencies]` + `[dev-dependencies]` override
6441 // convention is preserved: a name appearing in *both* lists is
6442 // valid (the dev-pin overrides at test/dev time). Only
6443 // within-list duplicates are structurally incoherent — pin the
6444 // permissive cross-list semantics so a future shortcut that
6445 // collapses the two seen-sets into one surfaces here as a test
6446 // failure.
6447 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6448 c.deps = vec![Dep::simple("caixa-teia", "^0.1")];
6449 c.deps_dev = vec![Dep::simple("caixa-teia", "^0.2")];
6450 c.validate_deps().unwrap();
6451 }
6452
6453 #[test]
6454 fn validate_deps_accepts_distinct_nome_in_both_lists() {
6455 // Positive control: distinct names within each list pass — the
6456 // gate's identity element on the canonical authoring shape.
6457 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6458 c.deps = vec![
6459 Dep::simple("caixa-teia", "^0.1"),
6460 Dep::simple("pleme-mesh", "*"),
6461 ];
6462 c.deps_dev = vec![
6463 Dep::simple("tatara-check", "*"),
6464 Dep::simple("dev-shim", "^0.1"),
6465 ];
6466 c.validate_deps().unwrap();
6467 }
6468
6469 #[test]
6470 fn validate_deps_per_entry_validate_fires_before_duplicate_in_deps() {
6471 // Diagnostic-precedence pin: a malformed `:versao` on the
6472 // duplicating entry surfaces its narrower `VersaoInvalid`
6473 // diagnostic first, before the cross-entry duplicate gate fires
6474 // — the canonical "per-entry shape before cross-entry uniqueness"
6475 // precedence every peer set-not-multiset gate establishes
6476 // (`*_invalid_fires_before_duplicate_check` pins on
6477 // `SupervisorSpec::validate`, `AplicacaoSpec::validate_membros`,
6478 // `validate_upgrade_from`).
6479 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6480 c.deps = vec![
6481 Dep::simple("caixa-teia", "^0.1"),
6482 Dep::simple("caixa-teia", "^bad-version"),
6483 ];
6484 let err = c.validate_deps().unwrap_err();
6485 assert!(
6486 matches!(
6487 err,
6488 crate::dep::DepError::VersaoInvalid { ref nome, ref versao, .. }
6489 if nome == "caixa-teia" && versao == "^bad-version"
6490 ),
6491 "expected VersaoInvalid to surface before DuplicateNome, got {err:?}"
6492 );
6493 }
6494
6495 #[test]
6496 fn validate_deps_duplicate_diagnostic_names_first_collision() {
6497 // First-collision determinism pin: with three entries naming the
6498 // same caixa, the first colliding pair surfaces — not the last.
6499 // Mirrors the peer first-collision posture on every
6500 // duplicate-target gate
6501 // (`validate_upgrade_from_duplicate_diagnostic_names_second_collision`
6502 // — the second entry is the first collision; this gate uses the
6503 // same shape: the second entry's `:nome` lands in the diagnostic
6504 // because `seen.insert(first.nome)` already populated the set).
6505 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6506 c.deps = vec![
6507 Dep::simple("caixa-teia", "^0.1"),
6508 Dep::simple("caixa-teia", "^0.2"),
6509 Dep::simple("caixa-teia", "^0.3"),
6510 ];
6511 let err = c.validate_deps().unwrap_err();
6512 // The diagnostic carries the offending caixa name; the
6513 // implementation surfaces on the *second* entry (the first
6514 // collision), so the test pins the `:nome` value.
6515 assert!(
6516 matches!(
6517 err,
6518 crate::dep::DepError::DuplicateNome { ref nome, list }
6519 if nome == "caixa-teia" && list == crate::render::DEP_AUTHOR_KEY_DEPS
6520 ),
6521 "got {err:?}"
6522 );
6523 }
6524
6525 #[test]
6526 fn validate_deps_duplicate_in_deps_fires_before_duplicate_in_deps_dev() {
6527 // Cross-list precedence pin: when both lists carry duplicates,
6528 // the `:deps` diagnostic surfaces first — same author-mental-
6529 // model ordering the `validate_deps_runs_deps_before_deps_dev`
6530 // pin establishes for malformed `:versao` (runtime axis before
6531 // dev axis).
6532 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6533 c.deps = vec![
6534 Dep::simple("runtime-dep", "^0.1"),
6535 Dep::simple("runtime-dep", "^0.2"),
6536 ];
6537 c.deps_dev = vec![Dep::simple("dev-dep", "*"), Dep::simple("dev-dep", "^0.1")];
6538 let err = c.validate_deps().unwrap_err();
6539 assert!(
6540 matches!(
6541 err,
6542 crate::dep::DepError::DuplicateNome { ref nome, list }
6543 if nome == "runtime-dep" && list == crate::render::DEP_AUTHOR_KEY_DEPS
6544 ),
6545 "expected :deps duplicate to surface before :deps-dev duplicate, got {err:?}"
6546 );
6547 }
6548
6549 #[test]
6550 fn validate_deps_empty_lists_pass_duplicate_gate() {
6551 // Empty-set identity pin: the bare template (zero deps, zero
6552 // deps_dev) passes the duplicate gate as the gate's identity
6553 // element. A future tighten that conflates "empty" with
6554 // "missing" would regress this baseline.
6555 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6556 c.validate_deps().unwrap();
6557 }
6558
6559 #[test]
6560 fn validate_deps_duplicate_diagnostic_carries_list_tag() {
6561 // Diagnostic-shape pin: the `list:` field tags which list the
6562 // duplicate landed in (`:deps` vs `:deps-dev`) verbatim, so a
6563 // `feira lint` run can route the author to the right block in
6564 // their caixa.lisp without re-deriving the list from context.
6565 // Same self-locating shape every peer per-axis diagnostic
6566 // already exposes.
6567 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6568 c.deps_dev = vec![
6569 Dep::simple("dev-thing", "*"),
6570 Dep::simple("dev-thing", "^0.1"),
6571 ];
6572 let err = c.validate_deps().unwrap_err();
6573 let crate::dep::DepError::DuplicateNome { nome, list } = err else {
6574 panic!("expected DuplicateNome from :deps-dev walk");
6575 };
6576 assert_eq!(nome, "dev-thing");
6577 assert_eq!(list, crate::render::DEP_AUTHOR_KEY_DEPS_DEV);
6578 }
6579
6580 // ── validate_deps: per-entry :caracteristicas set-discipline gate ──
6581
6582 #[test]
6583 fn validate_deps_surfaces_caracteristicas_duplicate_in_deps_list() {
6584 // Thread-through pin on `:deps`: the per-entry
6585 // `Dep::validate_caracteristicas` gate fires inside
6586 // `Caixa::validate_deps`'s linear walk, so a malformed feature
6587 // list on any `:deps` entry surfaces as a `DepError` from
6588 // `validate_deps` — the same reachability shape every per-entry
6589 // `Dep::validate` arm threads through. Without this pin a future
6590 // shortcut that skips the per-entry `Dep::validate` call on the
6591 // cross-entry-uniqueness path would mask the within-entry
6592 // `:caracteristicas` gates.
6593 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6594 c.deps = vec![Dep {
6595 nome: "caixa-teia".into(),
6596 versao: "^0.1".into(),
6597 fonte: None,
6598 opcional: false,
6599 caracteristicas: vec!["http".into(), "http".into()],
6600 }];
6601 let err = c.validate_deps().unwrap_err();
6602 let crate::dep::DepError::CaracteristicaDuplicate {
6603 nome,
6604 caracteristica,
6605 } = err
6606 else {
6607 panic!("expected CaracteristicaDuplicate from :deps walk, got {err:?}");
6608 };
6609 assert_eq!(nome, "caixa-teia");
6610 assert_eq!(caracteristica, "http");
6611 }
6612
6613 #[test]
6614 fn validate_deps_surfaces_caracteristicas_empty_in_deps_dev_list() {
6615 // Peer thread-through pin on `:deps-dev`: same reachability as
6616 // the `:deps` arm above, on the dev-only authoring axis. Pins
6617 // that the `validate_deps` walk visits both lists' per-entry
6618 // gates uniformly. The empty-feature arm carries here so both
6619 // new `:caracteristicas` arms are surfaced via at least one
6620 // `validate_deps` thread-through.
6621 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6622 c.deps_dev = vec![Dep {
6623 nome: "caixa-teia".into(),
6624 versao: "^0.1".into(),
6625 fonte: None,
6626 opcional: false,
6627 caracteristicas: vec![String::new()],
6628 }];
6629 let err = c.validate_deps().unwrap_err();
6630 let crate::dep::DepError::CaracteristicaEmpty { nome } = err else {
6631 panic!("expected CaracteristicaEmpty from :deps-dev walk, got {err:?}");
6632 };
6633 assert_eq!(nome, "caixa-teia");
6634 }
6635
6636 #[test]
6637 fn validate_deps_surfaces_caracteristicas_invalid_in_deps_list() {
6638 // Thread-through pin on `:deps`: the per-entry
6639 // `Dep::validate_caracteristicas` value-shape gate (lifted via
6640 // `crate::render::is_cargo_feature_name`) fires inside
6641 // `Caixa::validate_deps`'s linear walk on the `:deps` list, so
6642 // a structurally invalid feature name on any `:deps` entry
6643 // surfaces as `DepError::CaracteristicaInvalid` from
6644 // `validate_deps` — the same reachability shape every per-entry
6645 // `Dep::validate` arm threads through. Without this pin a
6646 // future shortcut that skips the per-entry `Dep::validate` call
6647 // on the cross-entry-uniqueness path would mask the within-
6648 // entry `:caracteristicas` value-shape gate.
6649 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6650 c.deps = vec![Dep {
6651 nome: "caixa-teia".into(),
6652 versao: "^0.1".into(),
6653 fonte: None,
6654 opcional: false,
6655 caracteristicas: vec!["+http".into()],
6656 }];
6657 let err = c.validate_deps().unwrap_err();
6658 let crate::dep::DepError::CaracteristicaInvalid {
6659 nome,
6660 caracteristica,
6661 ..
6662 } = err
6663 else {
6664 panic!("expected CaracteristicaInvalid from :deps walk, got {err:?}");
6665 };
6666 assert_eq!(nome, "caixa-teia");
6667 assert_eq!(caracteristica, "+http");
6668 }
6669
6670 #[test]
6671 fn validate_deps_surfaces_caracteristicas_invalid_in_deps_dev_list() {
6672 // Peer thread-through pin on `:deps-dev`: same reachability as
6673 // the `:deps` arm above, on the dev-only authoring axis. The
6674 // `http/json` shape carries here so the segment-separator
6675 // diagnostic (the canonical Cargo `dep/feat` namespaced-dep
6676 // confusion footgun) is surfaced via the cross-entry walk too —
6677 // pinning that the `:deps-dev` list visits the same per-entry
6678 // value-shape gate as the `:deps` list.
6679 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6680 c.deps_dev = vec![Dep {
6681 nome: "caixa-teia".into(),
6682 versao: "^0.1".into(),
6683 fonte: None,
6684 opcional: false,
6685 caracteristicas: vec!["http/json".into()],
6686 }];
6687 let err = c.validate_deps().unwrap_err();
6688 let crate::dep::DepError::CaracteristicaInvalid {
6689 nome,
6690 caracteristica,
6691 ..
6692 } = err
6693 else {
6694 panic!("expected CaracteristicaInvalid from :deps-dev walk, got {err:?}");
6695 };
6696 assert_eq!(nome, "caixa-teia");
6697 assert_eq!(caracteristica, "http/json");
6698 }
6699
6700 #[test]
6701 fn to_lisp_preserves_deps() {
6702 let src = r#"
6703(defcaixa
6704 :nome "x"
6705 :versao "0.1.0"
6706 :kind Biblioteca
6707 :deps ((:nome "a" :versao "^0.1")
6708 (:nome "b" :versao "*" :fonte (:tipo git :repo "github:o/b" :tag "v1"))))
6709"#;
6710 let c1 = Caixa::from_lisp(src).unwrap();
6711 let emitted = c1.to_lisp();
6712 let c2 = Caixa::from_lisp(&emitted).expect("round trip");
6713 assert_eq!(c1.deps, c2.deps);
6714 }
6715
6716 // ── Caixa::validate_nome — top-level :nome value-shape gate ─────────
6717
6718 fn caixa_with_nome(nome: &str) -> Caixa {
6719 let mut c = Caixa::from_lisp(&Caixa::template("placeholder")).unwrap();
6720 c.nome = nome.to_string();
6721 c
6722 }
6723
6724 #[test]
6725 fn validate_nome_accepts_canonical_template() {
6726 // Positive control: the bare `feira init`-style template's
6727 // `:nome` ("demo") is a canonical DNS-1123 label; the gate must
6728 // not regress this baseline shape. A future tightening of the
6729 // accepted set surfaces here as a test failure first.
6730 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6731 c.validate_nome().unwrap();
6732 }
6733
6734 #[test]
6735 fn validate_nome_accepts_canonical_forms() {
6736 // Positive-set sweep: each realistic caixa-name shape the K8s
6737 // apiserver accepts as a `metadata.name` label must pass —
6738 // single-word, hyphen-joined, version-suffixed, single-char,
6739 // two-char, digit-start (DNS-1123 allows this; the stricter
6740 // DNS-1035 Service-name rule doesn't), version-suffix-bearing.
6741 // Mirrors `accepts_canonical_membro_caixa_forms` (3f9d7a0) on
6742 // the peer member-name axis.
6743 for nome in [
6744 "checkout",
6745 "cart-v2",
6746 "a",
6747 "db",
6748 "3rd-party-shim",
6749 "payment-retry",
6750 "0",
6751 ] {
6752 caixa_with_nome(nome)
6753 .validate_nome()
6754 .unwrap_or_else(|e| panic!("canonical :nome {nome:?} must validate, got {e:?}"));
6755 }
6756 }
6757
6758 #[test]
6759 fn validate_nome_rejects_empty() {
6760 // Fail-before-pass-after pin: `Caixa::from_lisp` does not refuse
6761 // an empty `:nome` (the derive macro stores the raw String);
6762 // the gate's empty arm names the offending axis with a narrower
6763 // diagnostic than the `NomeInvalid` parse arm would emit.
6764 let c = caixa_with_nome("");
6765 let err = c.validate_nome().unwrap_err();
6766 assert_eq!(err, ManifestError::NomeEmpty);
6767 }
6768
6769 #[test]
6770 fn validate_nome_rejects_uppercase() {
6771 // The canonical "I copied the TitleCase display name verbatim"
6772 // footgun. The K8s apiserver rejects `metadata.name: MyApp` at
6773 // admission on every derived artifact (Helm chart, ComputeUnit,
6774 // CNP, HTTPRoute, label values); the gate moves the diagnostic
6775 // to the source `caixa.lisp` and the reason suggests the
6776 // lowercased fix verbatim.
6777 let c = caixa_with_nome("MyApp");
6778 let err = c.validate_nome().unwrap_err();
6779 let ManifestError::NomeInvalid { nome, reason } = err else {
6780 panic!("expected NomeInvalid for uppercase :nome");
6781 };
6782 assert_eq!(nome, "MyApp");
6783 assert!(
6784 reason.contains("uppercase") && reason.contains("myapp"),
6785 "diagnostic must name the violation + the lowercased fix, got {reason:?}"
6786 );
6787 }
6788
6789 #[test]
6790 fn validate_nome_rejects_underscore() {
6791 // The Python-/Postgres-style `snake_case` leak. DNS-1123 forbids
6792 // `_`; the apiserver rejects on admission across every derived
6793 // artifact. Same fixture pinned for `:membros :caixa` (3f9d7a0)
6794 // and `:children :caixa` (31bfa43).
6795 let c = caixa_with_nome("my_app");
6796 let err = c.validate_nome().unwrap_err();
6797 assert!(
6798 matches!(
6799 err,
6800 ManifestError::NomeInvalid { ref nome, ref reason }
6801 if nome == "my_app" && reason.contains('_')
6802 ),
6803 "got {err:?}"
6804 );
6805 }
6806
6807 #[test]
6808 fn validate_nome_rejects_dot() {
6809 // A `:nome` is a single DNS-1123 label, not a subdomain. The
6810 // "I want to namespace with `.`" footgun the gate redirects to
6811 // `-` via the shared predicate's reason wording.
6812 let c = caixa_with_nome("team.app");
6813 let err = c.validate_nome().unwrap_err();
6814 assert!(
6815 matches!(
6816 err,
6817 ManifestError::NomeInvalid { ref nome, ref reason }
6818 if nome == "team.app" && reason.contains('.')
6819 ),
6820 "got {err:?}"
6821 );
6822 }
6823
6824 #[test]
6825 fn validate_nome_rejects_leading_hyphen() {
6826 // DNS-1123 boundary rule: the label must start with an ASCII
6827 // alphanumeric. Pin the leading-`-` arm explicitly.
6828 let c = caixa_with_nome("-app");
6829 let err = c.validate_nome().unwrap_err();
6830 assert!(
6831 matches!(
6832 err,
6833 ManifestError::NomeInvalid { ref nome, .. } if nome == "-app"
6834 ),
6835 "got {err:?}"
6836 );
6837 }
6838
6839 #[test]
6840 fn validate_nome_rejects_trailing_hyphen() {
6841 // Symmetric arm of the boundary rule, pinned separately so a
6842 // future relaxation that only checks the leading position
6843 // surfaces here. Mirrors `rejects_membro_caixa_with_trailing_hyphen`
6844 // and `_with_trailing_hyphen` on the supervisor / aplicacao
6845 // axes.
6846 let c = caixa_with_nome("app-");
6847 let err = c.validate_nome().unwrap_err();
6848 assert!(
6849 matches!(
6850 err,
6851 ManifestError::NomeInvalid { ref nome, .. } if nome == "app-"
6852 ),
6853 "got {err:?}"
6854 );
6855 }
6856
6857 #[test]
6858 fn validate_nome_rejects_unicode() {
6859 // IDN must be pre-encoded as Punycode (`xn--…`); raw Unicode
6860 // bytes are rejected by the K8s apiserver on every name axis.
6861 let c = caixa_with_nome("café");
6862 let err = c.validate_nome().unwrap_err();
6863 assert!(
6864 matches!(
6865 err,
6866 ManifestError::NomeInvalid { ref nome, .. } if nome == "café"
6867 ),
6868 "got {err:?}"
6869 );
6870 }
6871
6872 #[test]
6873 fn validate_nome_rejects_whitespace() {
6874 // The paste-from-sketch / paste-from-spec footgun. Internal
6875 // whitespace is rejected by every K8s name axis.
6876 let c = caixa_with_nome("my app");
6877 let err = c.validate_nome().unwrap_err();
6878 assert!(
6879 matches!(
6880 err,
6881 ManifestError::NomeInvalid { ref nome, .. } if nome == "my app"
6882 ),
6883 "got {err:?}"
6884 );
6885 }
6886
6887 #[test]
6888 fn validate_nome_rejects_too_long() {
6889 // 64-byte boundary pin: the K8s apiserver rejects any
6890 // `metadata.name` over 63 bytes at admission; the diagnostic
6891 // names both the 63-byte cap and the actual length so the
6892 // author can shorten in one edit. Mirrors `_too_long` on the
6893 // peer member-/cluster-/child-name axes.
6894 let over = "a".repeat(crate::DNS_1123_LABEL_MAX_LEN + 1);
6895 let c = caixa_with_nome(&over);
6896 let err = c.validate_nome().unwrap_err();
6897 let ManifestError::NomeInvalid { nome, reason } = err else {
6898 panic!("expected NomeInvalid for over-cap :nome");
6899 };
6900 assert_eq!(nome.len(), crate::DNS_1123_LABEL_MAX_LEN + 1);
6901 assert!(
6902 reason.contains("63") && reason.contains("64"),
6903 "diagnostic must name the cap + actual length, got {reason:?}"
6904 );
6905 }
6906
6907 #[test]
6908 fn nome_max_length_validates() {
6909 // The 63-byte cap exactly — the boundary-accepting case pinned
6910 // alongside `validate_nome_rejects_too_long` so a future cap
6911 // shift surfaces both arms simultaneously. Mirrors
6912 // `membro_caixa_max_length_validates`,
6913 // `placement_cluster_max_length_validates`,
6914 // `child_caixa_max_length_validates`.
6915 let at_cap = "a".repeat(crate::DNS_1123_LABEL_MAX_LEN);
6916 caixa_with_nome(&at_cap).validate_nome().unwrap();
6917 }
6918
6919 #[test]
6920 fn nome_empty_takes_precedence_over_invalid() {
6921 // Order pin: the empty arm fires before the predicate is
6922 // consulted. Empty < invalid in self-locating-ness — the
6923 // narrower `NomeEmpty` diagnostic doesn't carry a useless
6924 // `nome: ""` reference into the parser-shaped reason. Mirrors
6925 // `membro_caixa_empty_takes_precedence_over_invalid` on the
6926 // peer axis (3f9d7a0).
6927 let c = caixa_with_nome("");
6928 assert_eq!(c.validate_nome().unwrap_err(), ManifestError::NomeEmpty);
6929 }
6930
6931 #[test]
6932 fn nome_invalid_diagnostic_carries_offending_nome() {
6933 // Diagnostic-shape pin: the error names the offending `:nome`
6934 // verbatim with a non-empty parser-shaped reason, so a `feira
6935 // lint` run can render the diagnostic without re-parsing.
6936 // Mirrors `membro_caixa_invalid_diagnostic_carries_offending_caixa`.
6937 let c = caixa_with_nome("MyApp");
6938 let err = c.validate_nome().unwrap_err();
6939 let ManifestError::NomeInvalid { nome, reason } = err else {
6940 panic!("expected NomeInvalid variant");
6941 };
6942 assert_eq!(nome, "MyApp");
6943 assert!(
6944 !reason.is_empty(),
6945 "NomeInvalid `reason` must carry the predicate's wording verbatim"
6946 );
6947 }
6948
6949 // ── Caixa::validate_nome_chart_name_budget — joint-length on `:nome` ──
6950 //
6951 // The bare-`:nome` axis [`Caixa::validate_nome`] caps at 63 bytes
6952 // via DNS-1123; this second-axis gate caps the joint
6953 // `lareira-<nome>` chart name at the same 63-byte ceiling. The
6954 // canonical [`crate::lareira_chart_name`] helper's doc comment
6955 // (f7320d7, caixa-core/src/render.rs:3198) explicitly deferred:
6956 // "the M4 admission webhook will pin the joint-length invariant
6957 // when it lands". These tests pin it at the manifest-validate
6958 // layer instead, fail-before-pass-after on the 56-byte boundary.
6959
6960 #[test]
6961 fn validate_nome_chart_name_budget_accepts_canonical_template() {
6962 // Positive control: the bare `feira init`-style template's
6963 // `:nome` ("demo") sits far below the cap; the gate must not
6964 // regress this baseline. Same shape every peer
6965 // value-shape-gate baseline pin uses.
6966 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6967 c.validate_nome_chart_name_budget().unwrap();
6968 }
6969
6970 #[test]
6971 fn validate_nome_chart_name_budget_accepts_canonical_fixtures() {
6972 // Positive-set sweep across the canonical author surface every
6973 // in-tree fixture uses (`hello-rio`, `cart`, `checkout`,
6974 // `worker`, the `checkout-aplicacao` example members, the
6975 // `akeyless-attest` caixa-tatara fixture). Every value sits
6976 // far below the 55-byte per-`:nome` budget. Same shape every
6977 // peer per-axis baseline pin uses.
6978 for nome in [
6979 "hello-rio",
6980 "cart",
6981 "checkout",
6982 "worker",
6983 "akeyless-attest",
6984 "demo",
6985 "a",
6986 ] {
6987 caixa_with_nome(nome)
6988 .validate_nome_chart_name_budget()
6989 .unwrap_or_else(|e| {
6990 panic!("canonical :nome {nome:?} must pass chart-name budget, got {e:?}")
6991 });
6992 }
6993 }
6994
6995 #[test]
6996 fn validate_nome_chart_name_budget_accepts_nome_at_cap() {
6997 // Boundary-accepting case at the 55-byte per-`:nome` budget —
6998 // the joint chart name is exactly 63 bytes, the DNS-1123 label
6999 // cap. Pinned alongside the rejecting-arm test so a future cap
7000 // shift surfaces both arms simultaneously. Mirrors
7001 // `nome_max_length_validates` on the peer bare-`:nome` axis.
7002 let at_cap = "a".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN);
7003 caixa_with_nome(&at_cap)
7004 .validate_nome_chart_name_budget()
7005 .unwrap();
7006 }
7007
7008 #[test]
7009 fn validate_nome_chart_name_budget_rejects_nome_one_over_cap() {
7010 // Fail-before-pass-after pin on the 56-byte boundary: the
7011 // smallest `:nome` length that overflows the joint chart-name
7012 // cap. The inner [`is_dns_1123_label`] gate
7013 // (`Caixa::validate_nome`) accepts it (56 ≤ 63), so prior to
7014 // this gate it silently passed the manifest-validate cascade
7015 // and surfaced as a `helm lint` / apiserver rejection on the
7016 // rendered chart name far from the source `caixa.lisp`, with
7017 // no field naming the overflow. With this gate the diagnostic
7018 // names the offending `:nome` verbatim alongside the rendered
7019 // chart name and the budget, so the author can shorten in one
7020 // edit. Mirrors `validate_nome_rejects_too_long` on the peer
7021 // bare-`:nome` axis.
7022 let over = "a".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 1);
7023 let c = caixa_with_nome(&over);
7024 let err = c.validate_nome_chart_name_budget().unwrap_err();
7025 let ManifestError::NomeChartNameBudgetExceeded { nome, reason } = err else {
7026 panic!("expected NomeChartNameBudgetExceeded for over-budget :nome");
7027 };
7028 assert_eq!(nome.len(), crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 1);
7029 assert_eq!(nome, over);
7030 assert!(
7031 reason.contains("63") && reason.contains("64") && reason.contains("55"),
7032 "diagnostic must name the DNS-1123 cap (63), the actual chart-name length (64), \
7033 and the per-`:nome` budget (55), got {reason:?}"
7034 );
7035 }
7036
7037 #[test]
7038 fn validate_nome_chart_name_budget_rejects_nome_at_bare_dns_cap() {
7039 // The 63-byte `:nome` boundary — passes the bare-`:nome`
7040 // [`is_dns_1123_label`] cap exactly, but produces a 71-byte
7041 // joint chart name that overflows the DNS-1123 label cap
7042 // structurally. The most stringent fail-before-pass-after
7043 // surface: every `:nome` in the 56..=63-byte range passed the
7044 // prior cascade and broke at admission.
7045 let bare_max = "a".repeat(crate::DNS_1123_LABEL_MAX_LEN);
7046 let c = caixa_with_nome(&bare_max);
7047 // The bare-`:nome` gate accepts the 63-byte length.
7048 c.validate_nome().unwrap();
7049 // The new joint-length gate rejects it.
7050 let err = c.validate_nome_chart_name_budget().unwrap_err();
7051 assert!(
7052 matches!(
7053 err,
7054 ManifestError::NomeChartNameBudgetExceeded { ref nome, .. }
7055 if nome.len() == crate::DNS_1123_LABEL_MAX_LEN
7056 ),
7057 "got {err:?}"
7058 );
7059 }
7060
7061 #[test]
7062 fn validate_nome_chart_name_budget_diagnostic_carries_offending_chart_name() {
7063 // Diagnostic-shape pin: the rendered `lareira-<nome>` chart
7064 // name appears verbatim in the diagnostic so the author sees
7065 // exactly the string the apiserver / `helm lint` would have
7066 // rejected — no re-derivation required to grep the source.
7067 // Peer with `nome_invalid_diagnostic_carries_offending_nome`
7068 // on the bare-`:nome` axis.
7069 let over = "x".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 5);
7070 let c = caixa_with_nome(&over);
7071 let err = c.validate_nome_chart_name_budget().unwrap_err();
7072 let ManifestError::NomeChartNameBudgetExceeded { nome, reason } = err else {
7073 panic!("expected NomeChartNameBudgetExceeded variant");
7074 };
7075 assert_eq!(nome, over);
7076 let expected_chart = crate::lareira_chart_name(&over);
7077 assert!(
7078 reason.contains(&expected_chart),
7079 "diagnostic must carry the rendered chart name {expected_chart:?} verbatim, \
7080 got {reason:?}"
7081 );
7082 assert!(
7083 reason.contains("lareira-"),
7084 "diagnostic must name the canonical chart-name prefix verbatim, got {reason:?}"
7085 );
7086 }
7087
7088 #[test]
7089 fn validate_nome_chart_name_budget_runs_after_nome_shape_via_layout_verify() {
7090 // Order pin on the layout cascade: the narrower
7091 // `NomeInvalid` (bare-DNS-1123 shape) fires before the
7092 // joint-length budget. A structurally-malformed `:nome` (here:
7093 // uppercase) surfaces its specific shape error rather than
7094 // the chart-name-budget error, even when the joint length
7095 // would also overflow — the narrower diagnostic is more
7096 // self-locating. Mirrors the cascade-precedence pins peer
7097 // gates already use (e.g. `EntradaParaEmpty` before
7098 // `EntradaParaInvalid`).
7099 let over = "A".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 1);
7100 let c = caixa_with_nome(&over);
7101 // The bare-shape gate fires first.
7102 let err = c.validate_nome().unwrap_err();
7103 assert!(
7104 matches!(err, ManifestError::NomeInvalid { .. }),
7105 "bare-shape gate must fire before chart-name-budget gate; got {err:?}"
7106 );
7107 // And the layout verify cascade surfaces that diagnostic, not
7108 // the budget arm. Inject a path-exists oracle so the cascade
7109 // gets past the manifest-presence check and into the
7110 // value-shape gates.
7111 let layout = crate::StandardLayout::new().with_path_exists(|_| true);
7112 let err = crate::LayoutInvariants::verify(
7113 &layout,
7114 &c,
7115 std::path::Path::new("/tmp/caixa-test-fake-root"),
7116 )
7117 .unwrap_err();
7118 let issue = err.to_string();
7119 assert!(
7120 issue.contains("DNS-1123") || issue.contains("uppercase"),
7121 "layout cascade must surface the bare-DNS-1123 diagnostic on a \
7122 structurally-malformed :nome, not the chart-name-budget diagnostic; got {issue:?}"
7123 );
7124 }
7125
7126 #[test]
7127 fn layout_verify_routes_chart_name_budget_through_nome_violation() {
7128 // Cross-axis envelope pin: the layout cascade wraps both
7129 // bare-`:nome` and joint-length-`:nome` failures through the
7130 // same [`LayoutError::NomeViolation`] envelope, since both
7131 // arms are on the `:nome` axis. The user's diagnostic stays
7132 // self-locating ("which axis"), and a future consumer that
7133 // dispatches on the layout-error variant (e.g. a `feira lint`
7134 // exit-code mapping) sees a single per-axis envelope. The
7135 // wrapped `issue:` carries the full inner diagnostic.
7136 let over = "a".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 1);
7137 let c = caixa_with_nome(&over);
7138 // The bare-shape gate accepts.
7139 c.validate_nome().unwrap();
7140 let layout = crate::StandardLayout::new().with_path_exists(|_| true);
7141 let err = crate::LayoutInvariants::verify(
7142 &layout,
7143 &c,
7144 std::path::Path::new("/tmp/caixa-test-fake-root"),
7145 )
7146 .unwrap_err();
7147 let crate::LayoutError::NomeViolation { caixa, issue } = err else {
7148 panic!("expected LayoutError::NomeViolation, got {err:?}");
7149 };
7150 assert_eq!(caixa, over);
7151 assert!(
7152 issue.contains("lareira-") && issue.contains("63") && issue.contains("55"),
7153 "wrapped issue must carry the joint-length diagnostic verbatim, got {issue:?}"
7154 );
7155 }
7156
7157 // ── Caixa::validate_versao — top-level :versao value-shape gate ─────
7158
7159 fn caixa_with_versao(versao: &str) -> Caixa {
7160 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
7161 c.versao = versao.to_string();
7162 c
7163 }
7164
7165 #[test]
7166 fn validate_versao_accepts_canonical_template() {
7167 // Positive control: the bare `feira init`-style template's
7168 // `:versao` ("0.1.0") is a canonical SemVer-2 literal; the gate
7169 // must not regress this baseline shape. A future tightening of
7170 // the accepted set surfaces here as a test failure first.
7171 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
7172 c.validate_versao().unwrap();
7173 }
7174
7175 #[test]
7176 fn validate_versao_accepts_canonical_forms() {
7177 // Positive-set sweep: each realistic SemVer-2 shape the
7178 // substrate's downstream consumers accept must pass — bare
7179 // MAJOR.MINOR.PATCH, pre-release tags (`-rc.1`, `-alpha.0`),
7180 // build metadata (`+build.42`), the combined form, and the
7181 // `0.0.0` boundary case. Mirrors `accepts_canonical_forms` on
7182 // the peer `:nome` axis (6c992f8).
7183 for versao in [
7184 "0.1.0",
7185 "0.0.0",
7186 "1.0.0",
7187 "0.2.0-rc.1",
7188 "1.0.0-alpha.0",
7189 "1.0.0+build.42",
7190 "1.0.0-rc.1+build.42",
7191 "10.20.30",
7192 ] {
7193 caixa_with_versao(versao)
7194 .validate_versao()
7195 .unwrap_or_else(|e| {
7196 panic!("canonical :versao {versao:?} must validate, got {e:?}")
7197 });
7198 }
7199 }
7200
7201 #[test]
7202 fn validate_versao_rejects_empty() {
7203 // Fail-before-pass-after pin: `Caixa::from_lisp` does not refuse
7204 // an empty `:versao` (the derive macro stores the raw String);
7205 // the gate's empty arm names the offending axis with a narrower
7206 // diagnostic than the `VersaoInvalid` parse arm would emit.
7207 // Mirrors `validate_nome_rejects_empty` (6c992f8).
7208 let c = caixa_with_versao("");
7209 let err = c.validate_versao().unwrap_err();
7210 assert_eq!(err, ManifestError::VersaoEmpty);
7211 }
7212
7213 #[test]
7214 fn validate_versao_rejects_git_tag_shape() {
7215 // The canonical "I copied the git tag verbatim" footgun —
7216 // `feira publish` *emits* `v<versao>` git tags, so a leaked
7217 // `v0.1.0` in `:versao` would render as `vv0.1.0` and silently
7218 // shift every downstream consumer's version axis. `semver`
7219 // rejects the leading `v` at parse time; the gate moves the
7220 // diagnostic to the source `caixa.lisp`.
7221 let c = caixa_with_versao("v0.1.0");
7222 let err = c.validate_versao().unwrap_err();
7223 let ManifestError::VersaoInvalid { versao, reason } = err else {
7224 panic!("expected VersaoInvalid for git-tag-shape :versao");
7225 };
7226 assert_eq!(versao, "v0.1.0");
7227 assert!(
7228 !reason.is_empty(),
7229 "VersaoInvalid `reason` must carry the parser's wording, got {reason:?}"
7230 );
7231 }
7232
7233 #[test]
7234 fn validate_versao_rejects_missing_patch() {
7235 // The canonical "I shortened it" footgun — SemVer-2 requires
7236 // three parts. Cargo's `version =` field accepts the shortened
7237 // form as a requirement, conflating the two leaks across the
7238 // typed `:deps :versao` vs top-level `:versao` axes; the gate
7239 // pins the top-level axis to the strict three-part shape.
7240 let c = caixa_with_versao("0.1");
7241 let err = c.validate_versao().unwrap_err();
7242 assert!(
7243 matches!(
7244 err,
7245 ManifestError::VersaoInvalid { ref versao, .. } if versao == "0.1"
7246 ),
7247 "got {err:?}"
7248 );
7249 }
7250
7251 #[test]
7252 fn validate_versao_rejects_requirement_shape() {
7253 // The canonical "I leaked a requirement into a version" footgun —
7254 // the typed `:deps :versao` / `:membros :versao` axes accept
7255 // `^0.1` (a `VersionReq`); the top-level `:versao` requires a
7256 // concrete `Version`. Without this gate the two typed surfaces
7257 // would silently overlap, and a top-level `^0.1` would surface
7258 // at `helm install` time as a Chart.yaml version rejection far
7259 // from the source `caixa.lisp`.
7260 let c = caixa_with_versao("^0.1");
7261 let err = c.validate_versao().unwrap_err();
7262 assert!(
7263 matches!(
7264 err,
7265 ManifestError::VersaoInvalid { ref versao, .. } if versao == "^0.1"
7266 ),
7267 "got {err:?}"
7268 );
7269 }
7270
7271 #[test]
7272 fn validate_versao_rejects_docker_tag_shape() {
7273 // The "I confused it with a docker tag" footgun — `latest`,
7274 // `main`, `stable` parse as identifiers, not SemVer-2 versions.
7275 // SemVer rejects at parse time; the gate moves the diagnostic
7276 // to the source `caixa.lisp`.
7277 for bad in ["latest", "main", "stable"] {
7278 let c = caixa_with_versao(bad);
7279 let err = c.validate_versao().unwrap_err();
7280 assert!(
7281 matches!(
7282 err,
7283 ManifestError::VersaoInvalid { ref versao, .. } if versao == bad
7284 ),
7285 "got {err:?} for {bad:?}"
7286 );
7287 }
7288 }
7289
7290 #[test]
7291 fn validate_versao_rejects_four_part_form() {
7292 // The Java/Microsoft "MAJOR.MINOR.PATCH.BUILD" convention
7293 // SemVer-2 forbids. A leak from a non-SemVer ecosystem; the
7294 // semver crate rejects the extra `.0` at parse time.
7295 let c = caixa_with_versao("0.1.0.0");
7296 let err = c.validate_versao().unwrap_err();
7297 assert!(
7298 matches!(
7299 err,
7300 ManifestError::VersaoInvalid { ref versao, .. } if versao == "0.1.0.0"
7301 ),
7302 "got {err:?}"
7303 );
7304 }
7305
7306 #[test]
7307 fn versao_empty_takes_precedence_over_invalid() {
7308 // Order pin: the empty arm fires before the parser is consulted.
7309 // Empty < invalid in self-locating-ness — the narrower
7310 // `VersaoEmpty` diagnostic doesn't carry a useless `versao: ""`
7311 // reference into the parser-shaped reason. Mirrors
7312 // `nome_empty_takes_precedence_over_invalid` (6c992f8) on the
7313 // peer axis.
7314 let c = caixa_with_versao("");
7315 assert_eq!(c.validate_versao().unwrap_err(), ManifestError::VersaoEmpty);
7316 }
7317
7318 #[test]
7319 fn versao_invalid_diagnostic_carries_offending_versao() {
7320 // Diagnostic-shape pin: the error names the offending `:versao`
7321 // verbatim with a non-empty parser-shaped reason, so a `feira
7322 // lint` run can render the diagnostic without re-parsing.
7323 // Mirrors `nome_invalid_diagnostic_carries_offending_nome`.
7324 let c = caixa_with_versao("v0.1.0");
7325 let err = c.validate_versao().unwrap_err();
7326 let ManifestError::VersaoInvalid { versao, reason } = err else {
7327 panic!("expected VersaoInvalid variant");
7328 };
7329 assert_eq!(versao, "v0.1.0");
7330 assert!(
7331 !reason.is_empty(),
7332 "VersaoInvalid `reason` must carry the parser's wording verbatim"
7333 );
7334 }
7335
7336 #[test]
7337 fn validate_versao_accepts_what_upgrade_from_from_accepts() {
7338 // Parity pin: every shape `UpgradeFromEntry::validate` accepts
7339 // for `:upgrade-from :from` must also pass `validate_versao` —
7340 // the two `:versao`-typed surfaces (top-level `:versao`,
7341 // `:upgrade-from :from`) consume the *same* `semver::Version`
7342 // parser, so they must agree on the accepted set. Without this
7343 // pin, a future tightening of one axis could silently diverge
7344 // from the other. Mirrors the `:versao` requirement-axis
7345 // parity (`:deps`/`:deps-dev`/`:membros`/`:children`) the prior
7346 // commits established.
7347 for versao in ["0.1.0", "0.2.0-rc.1", "1.0.0+build.42"] {
7348 // From the canonical UpgradeFromEntry round-trip fixture
7349 // (`upgrade::tests::round_trip_load_module` peers).
7350 let entry = crate::UpgradeFromEntry {
7351 from: versao.to_string(),
7352 instructions: Vec::new(),
7353 };
7354 entry
7355 .validate()
7356 .unwrap_or_else(|e| panic!(":from {versao:?} must validate, got {e:?}"));
7357 caixa_with_versao(versao)
7358 .validate_versao()
7359 .unwrap_or_else(|e| {
7360 panic!(":versao {versao:?} must validate, got {e:?} — peer axis diverges")
7361 });
7362 }
7363 }
7364
7365 // ── Caixa::validate_restart_window — supervisor restart-window
7366 // folds through the shared `supervisor::duration_codec` ────────
7367
7368 fn caixa_with_restart_window(window: Option<&str>) -> Caixa {
7369 let mut c = Caixa::from_lisp(&Caixa::template("root")).unwrap();
7370 c.kind = CaixaKind::Supervisor;
7371 c.restart_window = window.map(str::to_string);
7372 c
7373 }
7374
7375 #[test]
7376 fn validate_restart_window_accepts_none() {
7377 // The canonical "omit the slot to express no reset" shape — a
7378 // `None` raw string is the absence of the typed
7379 // `:restart-window` slot, which is exactly the SupervisorSpec
7380 // "never reset" semantics. The gate must be a no-op here; a
7381 // future tightening that rejected `None` would force every
7382 // supervisor caixa to authoring-time pin a window even when
7383 // the OTP semantics call for none.
7384 caixa_with_restart_window(None)
7385 .validate_restart_window()
7386 .unwrap();
7387 }
7388
7389 #[test]
7390 fn validate_restart_window_accepts_canonical_forms() {
7391 // Positive-set sweep across the canonical authoring units the
7392 // shared `supervisor::duration_codec::parse` accepts —
7393 // matches the codec-side `parse_accepts_integer_canonical_units`
7394 // pin in supervisor::tests so a future codec-side tightening
7395 // surfaces simultaneously on both axes.
7396 for window in ["60s", "5m", "1h", "500ms", "30", "0s"] {
7397 caixa_with_restart_window(Some(window))
7398 .validate_restart_window()
7399 .unwrap_or_else(|e| {
7400 panic!("canonical :restart-window {window:?} must validate, got {e:?}")
7401 });
7402 }
7403 }
7404
7405 #[test]
7406 fn validate_restart_window_rejects_fractional_seconds() {
7407 // Fail-before-pass-after pin: the `"1.5s"` drift class (parses
7408 // as f64 to 1.5 → renders back as `"1500ms"` on first
7409 // serialize). Prior to the fold + this gate, the inline
7410 // `parse_window_inline` accepted f64 magnitudes and silently
7411 // produced a `Duration::from_secs_f64(1.5)`, divergent from
7412 // the shared codec's integer-magnitude discipline on the
7413 // serde-routed siblings. The gate now surfaces a self-locating
7414 // diagnostic at the manifest layer.
7415 let err = caixa_with_restart_window(Some("1.5s"))
7416 .validate_restart_window()
7417 .unwrap_err();
7418 let ManifestError::RestartWindowMalformed {
7419 restart_window,
7420 reason,
7421 } = err
7422 else {
7423 panic!("expected RestartWindowMalformed for fractional seconds");
7424 };
7425 assert_eq!(restart_window, "1.5s");
7426 assert!(
7427 reason.contains("\"1.5\"") && reason.contains("not a non-negative integer"),
7428 "diagnostic must carry shared-codec wording, got {reason:?}"
7429 );
7430 }
7431
7432 #[test]
7433 fn validate_restart_window_rejects_decimal_shaped_integer() {
7434 // The `"1.0s"` class — numerically `1s` exactly, but the
7435 // canonical form is `"1s"` not `"1.0s"`. Decimal-shape leak
7436 // gets the same canonical-form diagnostic.
7437 let err = caixa_with_restart_window(Some("1.0s"))
7438 .validate_restart_window()
7439 .unwrap_err();
7440 assert!(
7441 matches!(
7442 err,
7443 ManifestError::RestartWindowMalformed { ref restart_window, .. }
7444 if restart_window == "1.0s"
7445 ),
7446 "got {err:?}"
7447 );
7448 }
7449
7450 #[test]
7451 fn validate_restart_window_rejects_half_unit_minute() {
7452 // `"0.5m"` is the unit-fraction footgun — author writes a
7453 // human-readable half-minute, the prior inline parser silently
7454 // produced `Duration::from_secs_f64(30.0)` and serde
7455 // re-emitted as `"30s"`, rewriting author intent. The gate
7456 // closes the loop at the manifest layer.
7457 let err = caixa_with_restart_window(Some("0.5m"))
7458 .validate_restart_window()
7459 .unwrap_err();
7460 let ManifestError::RestartWindowMalformed {
7461 restart_window,
7462 reason,
7463 } = err
7464 else {
7465 panic!("expected RestartWindowMalformed");
7466 };
7467 assert_eq!(restart_window, "0.5m");
7468 assert!(
7469 reason.contains("\"30s\""),
7470 "diagnostic must point at the canonical-form remediation, got {reason:?}"
7471 );
7472 }
7473
7474 #[test]
7475 fn validate_restart_window_rejects_leading_sign() {
7476 // `"+30s"` and `"-30s"` both round-tripped through f64 cleanly
7477 // on the prior parser (`+30` parses as `30.0`; `-30` parsed
7478 // and was caught by the `num < 0.0` arm which silently
7479 // returned `None`, dropping the author-supplied window). The
7480 // shared codec's digit-only gate rejects both with a unified
7481 // canonical-form diagnostic; the manifest-layer wrapper names
7482 // the offending value.
7483 for bad in ["+30s", "-30s"] {
7484 let err = caixa_with_restart_window(Some(bad))
7485 .validate_restart_window()
7486 .unwrap_err();
7487 assert!(
7488 matches!(
7489 err,
7490 ManifestError::RestartWindowMalformed { ref restart_window, .. }
7491 if restart_window == bad
7492 ),
7493 "got {err:?} for {bad:?}"
7494 );
7495 }
7496 }
7497
7498 #[test]
7499 fn validate_restart_window_rejects_unknown_unit() {
7500 // `"30x"` — the typo / wrong-unit footgun. The shared codec's
7501 // unit dispatch surfaces an `unknown duration unit` reason;
7502 // the manifest-layer wrapper names the offending value.
7503 let err = caixa_with_restart_window(Some("30x"))
7504 .validate_restart_window()
7505 .unwrap_err();
7506 let ManifestError::RestartWindowMalformed {
7507 restart_window,
7508 reason,
7509 } = err
7510 else {
7511 panic!("expected RestartWindowMalformed for unknown unit");
7512 };
7513 assert_eq!(restart_window, "30x");
7514 assert!(
7515 reason.contains("unknown duration unit"),
7516 "diagnostic must carry shared-codec unit-rejection wording, got {reason:?}"
7517 );
7518 }
7519
7520 #[test]
7521 fn validate_restart_window_rejects_garbage() {
7522 // Pure non-numeric magnitude (`"abc"`) falls through to the
7523 // shared codec's narrower `"bad duration magnitude"` arm. Same
7524 // diagnostic shape as the codec-side
7525 // `parse_garbage_still_falls_through_to_bad_magnitude` pin.
7526 let err = caixa_with_restart_window(Some("abc"))
7527 .validate_restart_window()
7528 .unwrap_err();
7529 let ManifestError::RestartWindowMalformed {
7530 restart_window,
7531 reason,
7532 } = err
7533 else {
7534 panic!("expected RestartWindowMalformed for garbage");
7535 };
7536 assert_eq!(restart_window, "abc");
7537 assert!(
7538 reason.contains("bad duration magnitude"),
7539 "diagnostic must carry shared-codec garbage-rejection wording, got {reason:?}"
7540 );
7541 }
7542
7543 #[test]
7544 fn validate_restart_window_rejects_empty_string() {
7545 // The empty-after-trim edge case — distinct from the `None`
7546 // canonical "omit the slot" shape. The shared codec's
7547 // digit-only gate refuses an empty magnitude; the manifest
7548 // layer names the offending `""` so the author can grep for
7549 // the literal empty value in their `caixa.lisp` and either
7550 // remove the slot (the canonical "no reset" shape) or pin a
7551 // positive duration.
7552 let err = caixa_with_restart_window(Some(""))
7553 .validate_restart_window()
7554 .unwrap_err();
7555 assert!(
7556 matches!(
7557 err,
7558 ManifestError::RestartWindowMalformed { ref restart_window, .. }
7559 if restart_window.is_empty()
7560 ),
7561 "got {err:?}"
7562 );
7563 }
7564
7565 #[test]
7566 fn validate_restart_window_diagnostic_carries_offending_value() {
7567 // Diagnostic-shape pin (peer with
7568 // `nome_invalid_diagnostic_carries_offending_nome` /
7569 // `versao_invalid_diagnostic_carries_offending_versao`): the
7570 // error names the offending raw `:restart-window` verbatim
7571 // with a non-empty shared-codec-shaped reason, so a `feira
7572 // lint` run can render the diagnostic without re-parsing.
7573 let err = caixa_with_restart_window(Some("1.5s"))
7574 .validate_restart_window()
7575 .unwrap_err();
7576 let ManifestError::RestartWindowMalformed {
7577 restart_window,
7578 reason,
7579 } = err
7580 else {
7581 panic!("expected RestartWindowMalformed variant");
7582 };
7583 assert_eq!(restart_window, "1.5s");
7584 assert!(
7585 !reason.is_empty(),
7586 "RestartWindowMalformed `reason` must carry the codec's wording verbatim"
7587 );
7588 }
7589
7590 #[test]
7591 fn supervisor_view_folds_through_shared_codec_on_canonical_form() {
7592 // Behavioral parity pin after the fold (`parse_window_inline`
7593 // deletion): the canonical `"60s"` still produces
7594 // `Duration::from_secs(60)` on the typed view — the fold is
7595 // semantically equivalent to the prior inline parser on the
7596 // accepted set. Mirrors the pre-fold `supervisor_view_returns_typed_shape`
7597 // pin, narrowed to the parser-side contract.
7598 let c = caixa_with_restart_window(Some("60s"));
7599 let view = c.supervisor_view().expect("Supervisor kind has a view");
7600 assert_eq!(
7601 view.restart_window,
7602 Some(std::time::Duration::from_secs(60))
7603 );
7604 }
7605
7606 #[test]
7607 fn supervisor_view_soft_swallows_what_validate_rejects() {
7608 // Parity pin between the view-construction path and the
7609 // manifest-level validator: the same `"1.5s"` that surfaces
7610 // `RestartWindowMalformed` at `validate_restart_window` time
7611 // becomes `restart_window: None` on the typed view (the fold
7612 // preserves the existing best-effort shape of `supervisor_view`).
7613 // The contract is: a layout-verifier / `feira lint` flow that
7614 // cares about the malformed-window axis MUST consult
7615 // `validate_restart_window` — relying solely on the view's
7616 // `None` swallows the diagnostic silently. This pin makes the
7617 // expectation a typed invariant.
7618 let c = caixa_with_restart_window(Some("1.5s"));
7619 let view = c.supervisor_view().expect("Supervisor kind has a view");
7620 assert_eq!(
7621 view.restart_window, None,
7622 "view-construction path soft-swallows the parse error to None"
7623 );
7624 // And the manifest-level validator does NOT soft-swallow:
7625 assert!(
7626 matches!(
7627 c.validate_restart_window().unwrap_err(),
7628 ManifestError::RestartWindowMalformed { ref restart_window, .. }
7629 if restart_window == "1.5s"
7630 ),
7631 "validator must surface the offending value",
7632 );
7633 }
7634
7635 // ── validate_code_paths — per-entry shape on :bibliotecas / :exe / :servicos ──
7636
7637 fn caixa_with_code_paths(bibliotecas: Vec<&str>, exe: Vec<&str>, servicos: Vec<&str>) -> Caixa {
7638 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
7639 c.bibliotecas = bibliotecas.into_iter().map(String::from).collect();
7640 c.exe = exe.into_iter().map(String::from).collect();
7641 c.servicos = servicos.into_iter().map(String::from).collect();
7642 c
7643 }
7644
7645 #[test]
7646 fn validate_code_paths_accepts_canonical_template() {
7647 // The bare `Caixa::template` shape is the gate's identity element
7648 // on the canonical authoring shape — `:bibliotecas
7649 // ("lib/demo.lisp")` + empty `:exe` + empty `:servicos`. Pins
7650 // that the gate is non-disruptive against every existing caixa.
7651 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
7652 c.validate_code_paths().unwrap();
7653 }
7654
7655 #[test]
7656 fn validate_code_paths_accepts_explicit_relative_paths_on_every_slot() {
7657 // Positive control sweep: a canonical-shaped path on every slot
7658 // passes. Mirrors the peer
7659 // `behavior::validate_every_slot_relative_is_ok` pin.
7660 let c = caixa_with_code_paths(
7661 vec!["lib/demo.lisp", "lib/helpers.lisp"],
7662 vec!["exe/demo", "exe/tool"],
7663 vec!["servicos/demo.computeunit.yaml"],
7664 );
7665 c.validate_code_paths().unwrap();
7666 }
7667
7668 #[test]
7669 fn validate_code_paths_accepts_all_empty_lists() {
7670 // The empty-list identity element: every Caixa with no declared
7671 // code paths trivially passes (Supervisor / Aplicacao kinds rely
7672 // on this — the OwnCode gate already rejected them before the
7673 // path-shape gate runs in the layout, but the validator itself
7674 // must accept the empty shape).
7675 let c = caixa_with_code_paths(vec![], vec![], vec![]);
7676 c.validate_code_paths().unwrap();
7677 }
7678
7679 #[test]
7680 fn validate_code_paths_rejects_empty_bibliotecas_entry() {
7681 let c = caixa_with_code_paths(vec![""], vec![], vec![]);
7682 let err = c.validate_code_paths().unwrap_err();
7683 assert!(
7684 matches!(
7685 err,
7686 ManifestError::CodePathEmpty {
7687 slot: ":bibliotecas"
7688 }
7689 ),
7690 "got {err:?}",
7691 );
7692 }
7693
7694 #[test]
7695 fn validate_code_paths_rejects_empty_exe_entry() {
7696 let c = caixa_with_code_paths(vec![], vec![""], vec![]);
7697 let err = c.validate_code_paths().unwrap_err();
7698 assert!(
7699 matches!(err, ManifestError::CodePathEmpty { slot: ":exe" }),
7700 "got {err:?}",
7701 );
7702 }
7703
7704 #[test]
7705 fn validate_code_paths_rejects_empty_servicos_entry() {
7706 let c = caixa_with_code_paths(vec![], vec![], vec![""]);
7707 let err = c.validate_code_paths().unwrap_err();
7708 assert!(
7709 matches!(err, ManifestError::CodePathEmpty { slot: ":servicos" }),
7710 "got {err:?}",
7711 );
7712 }
7713
7714 #[test]
7715 fn validate_code_paths_rejects_absolute_bibliotecas_entry() {
7716 // `:bibliotecas` has no `starts_with(<dir>)` fence downstream,
7717 // so an absolute path that resolves on disk silently passes the
7718 // layout's existence check — the canonical sandbox-escape on
7719 // the biblioteca axis.
7720 let c = caixa_with_code_paths(vec!["/etc/passwd"], vec![], vec![]);
7721 let err = c.validate_code_paths().unwrap_err();
7722 let ManifestError::CodePathAbsolute { slot, path } = err else {
7723 panic!("expected CodePathAbsolute, got {err:?}");
7724 };
7725 assert_eq!(slot, ":bibliotecas");
7726 assert_eq!(path, PathBuf::from("/etc/passwd"));
7727 }
7728
7729 #[test]
7730 fn validate_code_paths_rejects_absolute_exe_entry() {
7731 let c = caixa_with_code_paths(vec![], vec!["/usr/bin/env"], vec![]);
7732 let err = c.validate_code_paths().unwrap_err();
7733 let ManifestError::CodePathAbsolute { slot, path } = err else {
7734 panic!("expected CodePathAbsolute, got {err:?}");
7735 };
7736 assert_eq!(slot, ":exe");
7737 assert_eq!(path, PathBuf::from("/usr/bin/env"));
7738 }
7739
7740 #[test]
7741 fn validate_code_paths_rejects_absolute_servicos_entry() {
7742 let c = caixa_with_code_paths(vec![], vec![], vec!["/var/servicos/x.yaml"]);
7743 let err = c.validate_code_paths().unwrap_err();
7744 let ManifestError::CodePathAbsolute { slot, path } = err else {
7745 panic!("expected CodePathAbsolute, got {err:?}");
7746 };
7747 assert_eq!(slot, ":servicos");
7748 assert_eq!(path, PathBuf::from("/var/servicos/x.yaml"));
7749 }
7750
7751 #[test]
7752 fn validate_code_paths_rejects_parent_escape_bibliotecas_leading() {
7753 // Canonical "I want a lib from a sibling caixa" footgun on the
7754 // biblioteca axis. `:bibliotecas` has no `starts_with` fence
7755 // downstream, so a leading `..` traverses to the parent of the
7756 // caixa root with no diagnostic at layout time if the resolved
7757 // target exists.
7758 let c = caixa_with_code_paths(vec!["../sibling/x.lisp"], vec![], vec![]);
7759 let err = c.validate_code_paths().unwrap_err();
7760 let ManifestError::CodePathParentEscape { slot, path } = err else {
7761 panic!("expected CodePathParentEscape, got {err:?}");
7762 };
7763 assert_eq!(slot, ":bibliotecas");
7764 assert_eq!(path, PathBuf::from("../sibling/x.lisp"));
7765 }
7766
7767 #[test]
7768 fn validate_code_paths_rejects_parent_escape_exe_mid_path() {
7769 // Mid-path `..` defeats the layout's component-aware
7770 // `starts_with(exe_dir)` fence — `root.join("exe/../../escape")`
7771 // `starts_with(<root>/exe)` is true, but the canonical resolution
7772 // lives outside the caixa root. Caught regardless of where the
7773 // `..` sits — mirrors the peer
7774 // `behavior::validate_rejects_parent_escape_mid_path` pin.
7775 let c = caixa_with_code_paths(vec![], vec!["exe/../../escape"], vec![]);
7776 let err = c.validate_code_paths().unwrap_err();
7777 let ManifestError::CodePathParentEscape { slot, path } = err else {
7778 panic!("expected CodePathParentEscape, got {err:?}");
7779 };
7780 assert_eq!(slot, ":exe");
7781 assert_eq!(path, PathBuf::from("exe/../../escape"));
7782 }
7783
7784 #[test]
7785 fn validate_code_paths_rejects_parent_escape_servicos_trailing() {
7786 let c = caixa_with_code_paths(vec![], vec![], vec!["servicos/foo/../../escape.yaml"]);
7787 let err = c.validate_code_paths().unwrap_err();
7788 let ManifestError::CodePathParentEscape { slot, path } = err else {
7789 panic!("expected CodePathParentEscape, got {err:?}");
7790 };
7791 assert_eq!(slot, ":servicos");
7792 assert_eq!(path, PathBuf::from("servicos/foo/../../escape.yaml"));
7793 }
7794
7795 #[test]
7796 fn validate_code_paths_cross_slot_precedence_bibliotecas_before_exe_before_servicos() {
7797 // Cross-slot precedence pin: `:bibliotecas` → `:exe` →
7798 // `:servicos`. A manifest with malformed entries on all three
7799 // surfaces surfaces the `:bibliotecas` defect first, mirroring
7800 // the canonical declaration order
7801 // `Caixa::declared_foreign_code_slots` already establishes for
7802 // the foreign-code-slot diagnostic.
7803 let c = caixa_with_code_paths(vec![""], vec![""], vec![""]);
7804 let err = c.validate_code_paths().unwrap_err();
7805 assert!(
7806 matches!(
7807 err,
7808 ManifestError::CodePathEmpty {
7809 slot: ":bibliotecas"
7810 }
7811 ),
7812 "got {err:?}",
7813 );
7814 }
7815
7816 #[test]
7817 fn validate_code_paths_within_slot_precedence_empty_before_absolute_before_parent_escape() {
7818 // Within-slot precedence pin: empty → absolute → parent-escape,
7819 // matching the [`PathShapeViolation`] arm-ordering every peer
7820 // `is_sandboxed_relative_path` caller follows (b0c8389
7821 // BehaviorSpec, 26da2c7 UpgradeInstruction::StateChange). A
7822 // `:bibliotecas` list whose first entry is empty *and* whose
7823 // later entries are absolute/parent-escape surfaces the empty
7824 // arm first, on the lexicographically-earliest offending entry.
7825 let c = caixa_with_code_paths(vec!["", "/etc/passwd", "../escape.lisp"], vec![], vec![]);
7826 let err = c.validate_code_paths().unwrap_err();
7827 assert!(
7828 matches!(
7829 err,
7830 ManifestError::CodePathEmpty {
7831 slot: ":bibliotecas"
7832 }
7833 ),
7834 "got {err:?}",
7835 );
7836 }
7837
7838 #[test]
7839 fn validate_code_paths_first_offender_per_slot_wins() {
7840 // Within a single slot, the first declaration-order offender
7841 // surfaces — pins that the gate is left-to-right deterministic
7842 // (peer of every `*_first_collision_*` pin on duplicate gates).
7843 let c = caixa_with_code_paths(
7844 vec!["lib/ok.lisp", "/etc/escape", "../also-escape"],
7845 vec![],
7846 vec![],
7847 );
7848 let err = c.validate_code_paths().unwrap_err();
7849 let ManifestError::CodePathAbsolute { slot, path } = err else {
7850 panic!("expected CodePathAbsolute, got {err:?}");
7851 };
7852 assert_eq!(slot, ":bibliotecas");
7853 assert_eq!(path, PathBuf::from("/etc/escape"));
7854 }
7855
7856 #[test]
7857 fn validate_code_paths_diagnostic_carries_offending_slot_and_path() {
7858 // Diagnostic-shape pin (peer with
7859 // `nome_invalid_diagnostic_carries_offending_nome` /
7860 // `versao_invalid_diagnostic_carries_offending_versao`): the
7861 // error's Display surfaces both the offending `:slot` tag and
7862 // the offending path verbatim, so a `feira lint` run can render
7863 // the diagnostic without re-parsing.
7864 let c = caixa_with_code_paths(vec!["/etc/passwd"], vec![], vec![]);
7865 let rendered = c.validate_code_paths().unwrap_err().to_string();
7866 assert!(
7867 rendered.contains(":bibliotecas"),
7868 "diagnostic must name the offending slot: {rendered}",
7869 );
7870 assert!(
7871 rendered.contains("/etc/passwd"),
7872 "diagnostic must quote the offending path: {rendered}",
7873 );
7874 }
7875
7876 #[test]
7877 fn validate_code_paths_rejects_duplicate_bibliotecas_entry() {
7878 // Canonical copy-paste-the-wrong-file footgun on the biblioteca
7879 // axis. Without the gate `feira build` re-parses the same lib
7880 // twice, wasting work and silently masking the author's intent
7881 // to declare a *second* biblioteca.
7882 let c = caixa_with_code_paths(vec!["lib/demo.lisp", "lib/demo.lisp"], vec![], vec![]);
7883 let err = c.validate_code_paths().unwrap_err();
7884 let ManifestError::CodePathDuplicate { slot, path } = err else {
7885 panic!("expected CodePathDuplicate, got {err:?}");
7886 };
7887 assert_eq!(slot, ":bibliotecas");
7888 assert_eq!(path, PathBuf::from("lib/demo.lisp"));
7889 }
7890
7891 #[test]
7892 fn validate_code_paths_rejects_duplicate_exe_entry() {
7893 // Same footgun on the Binario surface. The future `caixa-flake`
7894 // emitter that materializes each `:exe` entry as a flake
7895 // `packages.<name>` derivation would collide on the duplicate
7896 // package key — surfaced here at the typed-validate layer with a
7897 // self-locating diagnostic instead.
7898 let c = caixa_with_code_paths(vec![], vec!["exe/cli", "exe/cli"], vec![]);
7899 let err = c.validate_code_paths().unwrap_err();
7900 let ManifestError::CodePathDuplicate { slot, path } = err else {
7901 panic!("expected CodePathDuplicate, got {err:?}");
7902 };
7903 assert_eq!(slot, ":exe");
7904 assert_eq!(path, PathBuf::from("exe/cli"));
7905 }
7906
7907 #[test]
7908 fn validate_code_paths_rejects_duplicate_servicos_entry() {
7909 // Same footgun on the Servico surface. The peer caixa-helm /
7910 // caixa-flux renderers refuse `:servicos.len() != 1` with the
7911 // narrower `UnsupportedServicoCount` diagnostic, but that
7912 // diagnostic surfaces "too many servicos" without naming
7913 // "duplicate entry" — the typed self-locating framing only lands
7914 // at this gate.
7915 let c = caixa_with_code_paths(
7916 vec![],
7917 vec![],
7918 vec![
7919 "servicos/demo.computeunit.yaml",
7920 "servicos/demo.computeunit.yaml",
7921 ],
7922 );
7923 let err = c.validate_code_paths().unwrap_err();
7924 let ManifestError::CodePathDuplicate { slot, path } = err else {
7925 panic!("expected CodePathDuplicate, got {err:?}");
7926 };
7927 assert_eq!(slot, ":servicos");
7928 assert_eq!(path, PathBuf::from("servicos/demo.computeunit.yaml"));
7929 }
7930
7931 #[test]
7932 fn validate_code_paths_accepts_same_path_across_slots() {
7933 // Per-list scope pin: a `:bibliotecas` entry that happens to
7934 // collide with an `:exe` or `:servicos` entry as a *string* is
7935 // not a duplicate by this gate (each list gets its own HashSet),
7936 // mirroring the peer `:deps` ↔ `:deps-dev` per-list scope
7937 // (a `:nome` present in both lists is a legitimate dev-vs-runtime
7938 // shape on the dep axis). The structural `starts_with(<exe |
7939 // servicos>_dir)` fence at layout time prevents the realistic
7940 // cross-slot collision case from existing on disk, but the gate's
7941 // per-list scope is correct independent of that downstream fence.
7942 let c = caixa_with_code_paths(
7943 vec!["lib/x.lisp"],
7944 vec!["exe/x"],
7945 vec!["servicos/x.computeunit.yaml"],
7946 );
7947 c.validate_code_paths().unwrap();
7948 }
7949
7950 #[test]
7951 fn validate_code_paths_duplicate_fires_after_structural_checks_on_same_slot() {
7952 // Within-slot ordering pin: structural defects (empty / absolute
7953 // / parent-escape) fire before the duplicate gate on the same
7954 // slot. A `:bibliotecas ("" "lib/x.lisp" "lib/x.lisp")` shape
7955 // surfaces the narrower `CodePathEmpty` for the empty entry
7956 // first, not the duplicate on the later pair — same arm-ordering
7957 // every peer per-list duplicate gate uses (`:etiquetas` 360a499,
7958 // `:autores` 86c769b, `:deps` 359fba5).
7959 let c = caixa_with_code_paths(vec!["", "lib/x.lisp", "lib/x.lisp"], vec![], vec![]);
7960 let err = c.validate_code_paths().unwrap_err();
7961 assert!(
7962 matches!(
7963 err,
7964 ManifestError::CodePathEmpty {
7965 slot: ":bibliotecas"
7966 }
7967 ),
7968 "got {err:?}",
7969 );
7970 }
7971
7972 #[test]
7973 fn validate_code_paths_duplicate_in_bibliotecas_fires_before_duplicate_in_exe() {
7974 // Cross-slot ordering pin on the duplicate arm: `:bibliotecas`
7975 // duplicates surface before `:exe` duplicates, matching the
7976 // canonical `:bibliotecas` → `:exe` → `:servicos` declaration
7977 // order every peer per-slot diagnostic on this surface follows.
7978 let c = caixa_with_code_paths(
7979 vec!["lib/x.lisp", "lib/x.lisp"],
7980 vec!["exe/y", "exe/y"],
7981 vec![],
7982 );
7983 let err = c.validate_code_paths().unwrap_err();
7984 let ManifestError::CodePathDuplicate { slot, path } = err else {
7985 panic!("expected CodePathDuplicate, got {err:?}");
7986 };
7987 assert_eq!(slot, ":bibliotecas");
7988 assert_eq!(path, PathBuf::from("lib/x.lisp"));
7989 }
7990
7991 #[test]
7992 fn validate_code_paths_duplicate_diagnostic_carries_offending_slot_and_path() {
7993 // Diagnostic-shape pin (peer with
7994 // `validate_code_paths_diagnostic_carries_offending_slot_and_path`
7995 // on the structural arm): the duplicate-arm Display surfaces both
7996 // the offending `:slot` tag and the offending path verbatim, so a
7997 // `feira lint` run can render the diagnostic without re-parsing.
7998 let c = caixa_with_code_paths(
7999 vec![],
8000 vec![],
8001 vec![
8002 "servicos/demo.computeunit.yaml",
8003 "servicos/demo.computeunit.yaml",
8004 ],
8005 );
8006 let rendered = c.validate_code_paths().unwrap_err().to_string();
8007 assert!(
8008 rendered.contains(":servicos"),
8009 "diagnostic must name the offending slot: {rendered}",
8010 );
8011 assert!(
8012 rendered.contains("servicos/demo.computeunit.yaml"),
8013 "diagnostic must quote the offending path: {rendered}",
8014 );
8015 }
8016
8017 // ── validate_code_paths — `.lisp` extension gate on :bibliotecas ──
8018 //
8019 // The lifted [`crate::render::is_lisp_extension`] predicate (33cc830)
8020 // now gates `:bibliotecas` entries on the tatara-lisp-source file-type
8021 // contract. The `feira build` loop (`caixa-feira/src/cmd/build.rs:33`)
8022 // reads every declared `:bibliotecas` entry through `tatara_lisp::read`
8023 // at parse time — the same downstream consumer the peer `:behavior
8024 // :on-*` (c97815a, [`crate::BehaviorError::NonLispExtension`]) and
8025 // `:upgrade-from :state-change :script` (33cc830,
8026 // [`crate::UpgradeError::NonLispExtensionScript`]) axes route through.
8027 // `:exe` and `:servicos` are deliberately excluded — `:exe` is the
8028 // nix-built executable surface (`"exe/<name>"` shape per the canonical
8029 // [`crate::LayoutError::ExeOutsideDir`] error message and every
8030 // in-tree `caixa_with_code_paths` positive control), and `:servicos`
8031 // is the `.computeunit.yaml` ComputeUnit-CR axis.
8032
8033 #[test]
8034 fn validate_code_paths_rejects_no_extension_bibliotecas_entry() {
8035 // Canonical "I dragged the wrong file from the workspace tree"
8036 // footgun on the biblioteca axis. Without the gate `feira build`
8037 // hands the extensionless path to `tatara_lisp::read` and fails
8038 // with a parser-shaped diagnostic far from the source caixa.lisp,
8039 // with no field naming the offending `:bibliotecas` entry.
8040 for relpath in ["lib/demo", "demo", "lib/handlers/inner"] {
8041 let c = caixa_with_code_paths(vec![relpath], vec![], vec![]);
8042 let err = c.validate_code_paths().unwrap_err();
8043 let ManifestError::CodePathNonLispExtension { slot, path } = err else {
8044 panic!("expected CodePathNonLispExtension for {relpath:?}, got {err:?}");
8045 };
8046 assert_eq!(slot, ":bibliotecas");
8047 assert_eq!(path, PathBuf::from(relpath));
8048 }
8049 }
8050
8051 #[test]
8052 fn validate_code_paths_rejects_wrong_extension_bibliotecas_entry() {
8053 // Wrong-extension sweep across common authoring footguns. Same
8054 // sweep posture as the peer
8055 // `behavior::validate_rejects_wrong_extension` (c97815a) and
8056 // `upgrade::tests::state_change_rejects_wrong_extension_script`
8057 // (33cc830) cases.
8058 for relpath in [
8059 "lib/demo.rs",
8060 "lib/demo.txt",
8061 "lib/demo.md",
8062 "lib/demo.json",
8063 "lib/demo.yaml",
8064 "lib/demo.toml",
8065 "lib/demo.lisp.bak",
8066 "lib/demo.lispx",
8067 "lib/demo.lis",
8068 ] {
8069 let c = caixa_with_code_paths(vec![relpath], vec![], vec![]);
8070 let err = c.validate_code_paths().unwrap_err();
8071 let ManifestError::CodePathNonLispExtension { slot, path } = err else {
8072 panic!("expected CodePathNonLispExtension for {relpath:?}, got {err:?}");
8073 };
8074 assert_eq!(slot, ":bibliotecas");
8075 assert_eq!(path, PathBuf::from(relpath));
8076 }
8077 }
8078
8079 #[test]
8080 fn validate_code_paths_rejects_case_folded_extension_bibliotecas_entry() {
8081 // Case-sensitivity sweep — pins the strict lowercase `.lisp`
8082 // contract. An uppercase `.LISP` shape that the layout's existence
8083 // check would (case-insensitively, on case-insensitive volumes)
8084 // match the on-disk file still mismatches the canonical form the
8085 // codec emits, breaking the THEORY.md §V.2.7 render-determinism
8086 // contract. Mirrors the peer
8087 // `behavior::validate_rejects_case_folded_extension` (c97815a) and
8088 // `upgrade::tests::state_change_rejects_case_folded_extension_script`
8089 // (33cc830) sweeps.
8090 for relpath in [
8091 "lib/demo.LISP",
8092 "lib/demo.Lisp",
8093 "lib/demo.LiSp",
8094 "lib/demo.lISP",
8095 ] {
8096 let c = caixa_with_code_paths(vec![relpath], vec![], vec![]);
8097 let err = c.validate_code_paths().unwrap_err();
8098 let ManifestError::CodePathNonLispExtension { slot, path } = err else {
8099 panic!("expected CodePathNonLispExtension for {relpath:?}, got {err:?}");
8100 };
8101 assert_eq!(slot, ":bibliotecas");
8102 assert_eq!(path, PathBuf::from(relpath));
8103 }
8104 }
8105
8106 #[test]
8107 fn validate_code_paths_accepts_canonical_lisp_shapes() {
8108 // Positive-control sweep through every canonical authoring shape
8109 // every in-tree fixture and the `Caixa::template` scaffold use.
8110 // Mirrors the peer `behavior::validate_accepts_canonical_lisp_paths`
8111 // (c97815a) and the lifted predicate's own
8112 // `is_lisp_extension_accepts_canonical_shapes` sweep in render.rs
8113 // (33cc830).
8114 for relpath in [
8115 "lib/demo.lisp",
8116 "lib/handlers.lisp",
8117 "lib/migrations/v01-to-v02.lisp",
8118 "demo.lisp",
8119 "a.lisp",
8120 "./lib/demo.lisp",
8121 "lib/./handlers.lisp",
8122 "lib/migrations/v.0.1.lisp",
8123 ] {
8124 let c = caixa_with_code_paths(vec![relpath], vec![], vec![]);
8125 c.validate_code_paths()
8126 .unwrap_or_else(|e| panic!("canonical shape {relpath:?} must pass, got {e:?}"));
8127 }
8128 }
8129
8130 #[test]
8131 fn validate_code_paths_non_lisp_extension_does_not_fire_on_exe_or_servicos() {
8132 // The file-type gate is per-slot — only `:bibliotecas` carries the
8133 // tatara-lisp-source contract. An extensionless `:exe` entry
8134 // (`exe/demo`) and a `.computeunit.yaml` `:servicos` entry are the
8135 // canonical shapes every in-tree fixture uses, and must continue
8136 // to pass validate. Pins that a future tightening that broadens
8137 // the `.lisp` gate to either axis surfaces as a test failure
8138 // rather than as a silent breaking change to existing valid
8139 // manifests.
8140 let c = caixa_with_code_paths(
8141 vec![],
8142 vec!["exe/demo", "exe/tool"],
8143 vec!["servicos/demo.computeunit.yaml"],
8144 );
8145 c.validate_code_paths().unwrap();
8146 }
8147
8148 #[test]
8149 fn validate_code_paths_sandbox_shape_arms_precede_non_lisp_extension() {
8150 // Cross-arm precedence pin: a `:bibliotecas` entry that is *both*
8151 // sandbox-escaping and non-`.lisp` surfaces the more fundamental
8152 // sandbox-shape diagnostic first (the `.lisp` remediation would
8153 // be misleading when the offending path can never resolve under
8154 // the caixa root anyway). Mirrors the peer
8155 // `EmptyPath` → `AbsolutePath` → `ParentEscape` → `NonLispExtension`
8156 // ordering on `:behavior :on-*` (c97815a) and `EmptyScript` →
8157 // `AbsoluteScript` → `ParentEscapeScript` → `NonLispExtensionScript`
8158 // on `:upgrade-from :state-change :script` (33cc830).
8159 //
8160 // Empty wins (the strictly-smaller-scope structural arm).
8161 let c = caixa_with_code_paths(vec![""], vec![], vec![]);
8162 assert!(
8163 matches!(
8164 c.validate_code_paths().unwrap_err(),
8165 ManifestError::CodePathEmpty {
8166 slot: ":bibliotecas"
8167 }
8168 ),
8169 "empty must win over non-lisp-extension",
8170 );
8171 // Absolute wins (the path can't resolve under the caixa root).
8172 let c = caixa_with_code_paths(vec!["/etc/passwd"], vec![], vec![]);
8173 let err = c.validate_code_paths().unwrap_err();
8174 let ManifestError::CodePathAbsolute { slot, .. } = err else {
8175 panic!("absolute must win over non-lisp-extension, got {err:?}");
8176 };
8177 assert_eq!(slot, ":bibliotecas");
8178 // ParentEscape wins (the path escapes the caixa root).
8179 let c = caixa_with_code_paths(vec!["../sibling/x.txt"], vec![], vec![]);
8180 let err = c.validate_code_paths().unwrap_err();
8181 let ManifestError::CodePathParentEscape { slot, .. } = err else {
8182 panic!("parent-escape must win over non-lisp-extension, got {err:?}");
8183 };
8184 assert_eq!(slot, ":bibliotecas");
8185 }
8186
8187 #[test]
8188 fn validate_code_paths_non_lisp_extension_precedes_duplicate() {
8189 // Within-slot precedence pin: the per-entry file-type shape gate
8190 // fires before the cross-entry duplicate gate, so the narrower
8191 // structural defect dominates the uniqueness diagnostic. A
8192 // `("lib/x.txt" "lib/x.txt")` shape surfaces
8193 // `CodePathNonLispExtension` on the first entry rather than
8194 // `CodePathDuplicate` on the pair — same posture every per-entry
8195 // shape-gate-precedes-duplicate cascade follows on this surface
8196 // (the empty / absolute / parent-escape arms already precede the
8197 // duplicate arm; the lifted file-type arm joins that set).
8198 let c = caixa_with_code_paths(vec!["lib/x.txt", "lib/x.txt"], vec![], vec![]);
8199 let err = c.validate_code_paths().unwrap_err();
8200 let ManifestError::CodePathNonLispExtension { slot, path } = err else {
8201 panic!("expected CodePathNonLispExtension, got {err:?}");
8202 };
8203 assert_eq!(slot, ":bibliotecas");
8204 assert_eq!(path, PathBuf::from("lib/x.txt"));
8205 }
8206
8207 #[test]
8208 fn validate_code_paths_non_lisp_extension_diagnostic_carries_offending_slot_and_path() {
8209 // Diagnostic-shape pin (peer with
8210 // `validate_code_paths_diagnostic_carries_offending_slot_and_path`
8211 // on the sandbox-shape arms and
8212 // `validate_code_paths_duplicate_diagnostic_carries_offending_slot_and_path`
8213 // on the duplicate arm): the file-type-arm Display surfaces both
8214 // the offending `:slot` tag, the offending path verbatim, and the
8215 // expected `.lisp` extension named in the remediation text, so a
8216 // `feira lint` run can render the diagnostic without re-parsing.
8217 let c = caixa_with_code_paths(vec!["lib/demo.rs"], vec![], vec![]);
8218 let rendered = c.validate_code_paths().unwrap_err().to_string();
8219 assert!(
8220 rendered.contains(":bibliotecas"),
8221 "diagnostic must name the offending slot: {rendered}",
8222 );
8223 assert!(
8224 rendered.contains("lib/demo.rs"),
8225 "diagnostic must quote the offending path: {rendered}",
8226 );
8227 assert!(
8228 rendered.contains(".lisp"),
8229 "diagnostic must name the expected extension: {rendered}",
8230 );
8231 }
8232
8233 // ── validate_code_paths — `.computeunit.yaml` compound-suffix gate on :servicos ──
8234 //
8235 // The lifted [`crate::render::is_computeunit_yaml_extension`] predicate
8236 // now gates `:servicos` entries on the ComputeUnit-CR YAML file-type
8237 // contract. The peer caixa-helm / caixa-flux renderers consume each
8238 // `:servicos` entry through `serde_yaml::from_str` as a typed
8239 // `ComputeUnit` CR — same downstream-consumer-shape lift as the peer
8240 // `:bibliotecas` `.lisp` gate (64772a9), here on the compound-suffix
8241 // axis `Path::extension` can't express on its own.
8242
8243 #[test]
8244 fn validate_code_paths_rejects_no_extension_servicos_entry() {
8245 // Canonical "I dragged the wrong file from the workspace tree"
8246 // footgun on the Servico axis. Without the gate the peer
8247 // caixa-helm / caixa-flux renderers hand the extensionless path
8248 // to `serde_yaml::from_str` and fail with a parser-shaped
8249 // diagnostic far from the source caixa.lisp, with no field
8250 // naming the offending `:servicos` entry.
8251 for relpath in ["servicos/demo", "demo", "servicos/sub/nested"] {
8252 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
8253 let err = c.validate_code_paths().unwrap_err();
8254 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
8255 panic!(
8256 "expected CodePathNonComputeUnitYamlExtension for {relpath:?}, \
8257 got {err:?}"
8258 );
8259 };
8260 assert_eq!(slot, ":servicos");
8261 assert_eq!(path, PathBuf::from(relpath));
8262 }
8263 }
8264
8265 #[test]
8266 fn validate_code_paths_rejects_wrong_extension_servicos_entry() {
8267 // Wrong-extension sweep across common authoring footguns on the
8268 // Servico axis. Bare `.yaml` is the canonical "I forgot the
8269 // `.computeunit` segment" typo; the off-by-one-segment shapes
8270 // (`.computeunit-yaml` / `.computeunit_yaml`) silently pass the
8271 // bare `Path::extension` view but mismatch the typed compound
8272 // suffix the renderers' `serde_yaml::from_str` consumer demands.
8273 // Same sweep-posture as the peer
8274 // `validate_code_paths_rejects_wrong_extension_bibliotecas_entry`
8275 // (64772a9) on the sibling tatara-lisp-source axis.
8276 for relpath in [
8277 "servicos/demo.yaml",
8278 "servicos/demo.yml",
8279 "servicos/demo.json",
8280 "servicos/demo.toml",
8281 "servicos/demo.txt",
8282 "servicos/demo.computeunit.yaml.bak",
8283 "servicos/demo.computeunit.yam",
8284 "servicos/demo.computeunit",
8285 "servicos/demo-computeunit.yaml",
8286 "servicos/demo_computeunit.yaml",
8287 ] {
8288 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
8289 let err = c.validate_code_paths().unwrap_err();
8290 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
8291 panic!(
8292 "expected CodePathNonComputeUnitYamlExtension for {relpath:?}, \
8293 got {err:?}"
8294 );
8295 };
8296 assert_eq!(slot, ":servicos");
8297 assert_eq!(path, PathBuf::from(relpath));
8298 }
8299 }
8300
8301 #[test]
8302 fn validate_code_paths_rejects_case_folded_extension_servicos_entry() {
8303 // Case-sensitivity sweep — pins the strict lowercase
8304 // `.computeunit.yaml` contract. A case-folded shape that the
8305 // layout's existence check would (case-insensitively, on
8306 // case-insensitive volumes) match the on-disk file still
8307 // mismatches the canonical form the codec emits, breaking the
8308 // THEORY.md §V.2.7 render-determinism contract. Mirrors the peer
8309 // `validate_code_paths_rejects_case_folded_extension_bibliotecas_entry`
8310 // (64772a9) sweep on the sibling tatara-lisp-source axis.
8311 for relpath in [
8312 "servicos/demo.ComputeUnit.yaml",
8313 "servicos/demo.COMPUTEUNIT.yaml",
8314 "servicos/demo.computeunit.YAML",
8315 "servicos/demo.computeunit.Yaml",
8316 "servicos/demo.COMPUTEUNIT.YAML",
8317 ] {
8318 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
8319 let err = c.validate_code_paths().unwrap_err();
8320 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
8321 panic!(
8322 "expected CodePathNonComputeUnitYamlExtension for {relpath:?}, \
8323 got {err:?}"
8324 );
8325 };
8326 assert_eq!(slot, ":servicos");
8327 assert_eq!(path, PathBuf::from(relpath));
8328 }
8329 }
8330
8331 #[test]
8332 fn validate_code_paths_rejects_empty_stem_servicos_entry() {
8333 // Degenerate hidden-file shape: a file name exactly equal to the
8334 // suffix (`.computeunit.yaml` — no stem preceding the suffix) is
8335 // the structural "Servico declared with no identity" footgun.
8336 // The substrate identifies each ComputeUnit by the file-stem
8337 // segment that precedes `.computeunit.yaml` (the rendered
8338 // `lareira-<stem>` Helm chart, the per-Servico `metadata.name`,
8339 // the M3 `:contratos` membership lookup), so an empty stem
8340 // leaves the Servico unidentifiable. Pinned at the typed-axis
8341 // level so a future regression that drops the `name.len() >
8342 // SUFFIX.len()` bound at the predicate surfaces here, not
8343 // piecemeal as a `lareira-` chart-name collision at render time.
8344 for relpath in ["servicos/.computeunit.yaml"] {
8345 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
8346 let err = c.validate_code_paths().unwrap_err();
8347 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
8348 panic!(
8349 "expected CodePathNonComputeUnitYamlExtension for {relpath:?}, \
8350 got {err:?}"
8351 );
8352 };
8353 assert_eq!(slot, ":servicos");
8354 assert_eq!(path, PathBuf::from(relpath));
8355 }
8356 }
8357
8358 #[test]
8359 fn validate_code_paths_accepts_canonical_computeunit_yaml_shapes() {
8360 // Positive-control sweep through every canonical authoring shape
8361 // every in-tree fixture and the `Caixa::template` scaffold use.
8362 // Mirrors the peer
8363 // `validate_code_paths_accepts_canonical_lisp_shapes` (64772a9)
8364 // and the lifted predicate's own
8365 // `computeunit_yaml_extension_accepts_canonical_shapes` sweep in
8366 // render.rs.
8367 for relpath in [
8368 "servicos/demo.computeunit.yaml",
8369 "servicos/hello-rio.computeunit.yaml",
8370 "servicos/my-service.computeunit.yaml",
8371 "servicos/a.computeunit.yaml",
8372 "./servicos/demo.computeunit.yaml",
8373 "servicos/./demo.computeunit.yaml",
8374 "servicos/sub/nested.computeunit.yaml",
8375 "servicos/v0.1.computeunit.yaml",
8376 ] {
8377 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
8378 c.validate_code_paths()
8379 .unwrap_or_else(|e| panic!("canonical shape {relpath:?} must pass, got {e:?}"));
8380 }
8381 }
8382
8383 #[test]
8384 fn validate_code_paths_non_computeunit_yaml_extension_does_not_fire_on_bibliotecas_or_exe() {
8385 // The file-type gate is per-slot — only `:servicos` carries the
8386 // ComputeUnit-CR YAML contract. A canonical `.lisp` `:bibliotecas`
8387 // entry and an extensionless `:exe` entry are the canonical
8388 // shapes every in-tree fixture uses, and must continue to pass
8389 // validate. Peer of
8390 // `validate_code_paths_non_lisp_extension_does_not_fire_on_exe_or_servicos`
8391 // (64772a9) — together pin that the typed
8392 // [`CodePathFileType`] dispatch is exhaustively per-slot, with no
8393 // cross-axis leakage in either direction.
8394 let c = caixa_with_code_paths(
8395 vec!["lib/demo.lisp"],
8396 vec!["exe/demo", "exe/tool"],
8397 vec!["servicos/demo.computeunit.yaml"],
8398 );
8399 c.validate_code_paths().unwrap();
8400 }
8401
8402 #[test]
8403 fn validate_code_paths_sandbox_shape_arms_precede_non_computeunit_yaml_extension() {
8404 // Cross-arm precedence pin: a `:servicos` entry that is *both*
8405 // sandbox-escaping and wrong-extension surfaces the more
8406 // fundamental sandbox-shape diagnostic first (the
8407 // `.computeunit.yaml` remediation would be misleading when the
8408 // offending path can never resolve under the caixa root
8409 // anyway). Mirrors the peer
8410 // `validate_code_paths_sandbox_shape_arms_precede_non_lisp_extension`
8411 // (64772a9) ordering on the sibling `:bibliotecas` axis and the
8412 // peer `EmptyPath` → `AbsolutePath` → `ParentEscape` →
8413 // `NonComputeUnitYamlExtension` arm-ordering the dispatch
8414 // table establishes.
8415 //
8416 // Empty wins (the strictly-smaller-scope structural arm).
8417 let c = caixa_with_code_paths(vec![], vec![], vec![""]);
8418 assert!(
8419 matches!(
8420 c.validate_code_paths().unwrap_err(),
8421 ManifestError::CodePathEmpty { slot: ":servicos" }
8422 ),
8423 "empty must win over non-computeunit-yaml-extension",
8424 );
8425 // Absolute wins (the path can't resolve under the caixa root).
8426 let c = caixa_with_code_paths(vec![], vec![], vec!["/etc/foo.yaml"]);
8427 let err = c.validate_code_paths().unwrap_err();
8428 let ManifestError::CodePathAbsolute { slot, .. } = err else {
8429 panic!("absolute must win over non-computeunit-yaml-extension, got {err:?}");
8430 };
8431 assert_eq!(slot, ":servicos");
8432 // ParentEscape wins (the path escapes the caixa root).
8433 let c = caixa_with_code_paths(vec![], vec![], vec!["../sibling/x.yaml"]);
8434 let err = c.validate_code_paths().unwrap_err();
8435 let ManifestError::CodePathParentEscape { slot, .. } = err else {
8436 panic!("parent-escape must win over non-computeunit-yaml-extension, got {err:?}");
8437 };
8438 assert_eq!(slot, ":servicos");
8439 }
8440
8441 #[test]
8442 fn validate_code_paths_non_computeunit_yaml_extension_precedes_duplicate() {
8443 // Within-slot precedence pin: the per-entry file-type shape gate
8444 // fires before the cross-entry duplicate gate, so the narrower
8445 // structural defect dominates the uniqueness diagnostic. A
8446 // `("servicos/x.yaml" "servicos/x.yaml")` shape surfaces
8447 // `CodePathNonComputeUnitYamlExtension` on the first entry
8448 // rather than `CodePathDuplicate` on the pair — same posture
8449 // every per-entry shape-gate-precedes-duplicate cascade follows
8450 // on this surface, peer of the 64772a9 `:bibliotecas`
8451 // `("lib/x.txt" "lib/x.txt")` ordering.
8452 let c = caixa_with_code_paths(vec![], vec![], vec!["servicos/x.yaml", "servicos/x.yaml"]);
8453 let err = c.validate_code_paths().unwrap_err();
8454 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
8455 panic!("expected CodePathNonComputeUnitYamlExtension, got {err:?}");
8456 };
8457 assert_eq!(slot, ":servicos");
8458 assert_eq!(path, PathBuf::from("servicos/x.yaml"));
8459 }
8460
8461 #[test]
8462 fn validate_code_paths_non_computeunit_yaml_extension_diagnostic_carries_offending_slot_and_path()
8463 {
8464 // Diagnostic-shape pin (peer with
8465 // `validate_code_paths_non_lisp_extension_diagnostic_carries_offending_slot_and_path`
8466 // on the sibling tatara-lisp-source axis): the file-type-arm
8467 // Display surfaces both the offending `:slot` tag, the
8468 // offending path verbatim, and the expected
8469 // `.computeunit.yaml` compound suffix named in the remediation
8470 // text, so a `feira lint` run can render the diagnostic without
8471 // re-parsing.
8472 let c = caixa_with_code_paths(vec![], vec![], vec!["servicos/demo.yaml"]);
8473 let rendered = c.validate_code_paths().unwrap_err().to_string();
8474 assert!(
8475 rendered.contains(":servicos"),
8476 "diagnostic must name the offending slot: {rendered}",
8477 );
8478 assert!(
8479 rendered.contains("servicos/demo.yaml"),
8480 "diagnostic must quote the offending path: {rendered}",
8481 );
8482 assert!(
8483 rendered.contains(".computeunit.yaml"),
8484 "diagnostic must name the expected compound suffix: {rendered}",
8485 );
8486 }
8487
8488 // ── validate_etiquetas — universal-axis registry-search-tag shape ──
8489
8490 fn caixa_with_etiquetas(etiquetas: Vec<&str>) -> Caixa {
8491 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
8492 c.etiquetas = etiquetas.into_iter().map(String::from).collect();
8493 c
8494 }
8495
8496 #[test]
8497 fn validate_etiquetas_accepts_empty_list() {
8498 // The empty-list identity: every caixa with no declared tags
8499 // trivially passes — `Caixa::template` emits `:etiquetas ()`,
8500 // so the gate is non-disruptive against every existing manifest.
8501 let c = caixa_with_etiquetas(vec![]);
8502 c.validate_etiquetas().unwrap();
8503 }
8504
8505 #[test]
8506 fn validate_etiquetas_accepts_canonical_forms() {
8507 // Positive control sweep: a canonical-shaped non-empty distinct
8508 // tag list passes, mirroring the example checkout-aplicacao
8509 // (`:etiquetas ("example" "aplicacao" "mesh" "ecommerce" "demo")`)
8510 // and the hello-rio fixture (`("hello-world" "wasm" "rust")`).
8511 let c = caixa_with_etiquetas(vec!["example", "aplicacao", "mesh", "ecommerce", "demo"]);
8512 c.validate_etiquetas().unwrap();
8513 }
8514
8515 #[test]
8516 fn validate_etiquetas_rejects_empty_entry() {
8517 // Canonical paste-from-blank-doc footgun. Without the gate the
8518 // empty entry rendered as `keywords: [""]` in `Chart.yaml`, a
8519 // no-op tag indexing nothing in the future caixa-registry.
8520 let c = caixa_with_etiquetas(vec![""]);
8521 let err = c.validate_etiquetas().unwrap_err();
8522 assert!(matches!(err, ManifestError::EtiquetaEmpty), "got {err:?}",);
8523 }
8524
8525 #[test]
8526 fn validate_etiquetas_rejects_duplicate_entry() {
8527 // Canonical copy-paste-the-wrong-tag footgun. Without the gate
8528 // the duplicate was silently dedup'd by caixa-helm's BTreeSet
8529 // collect at chart render — a "second wins / one silently
8530 // disappears" shape divergent from every peer typed-graph set
8531 // gate. The duplicate-arm names the offending tag verbatim.
8532 let c = caixa_with_etiquetas(vec!["demo", "demo"]);
8533 let err = c.validate_etiquetas().unwrap_err();
8534 let ManifestError::EtiquetaDuplicate { etiqueta } = err else {
8535 panic!("expected EtiquetaDuplicate, got {err:?}");
8536 };
8537 assert_eq!(etiqueta, "demo");
8538 }
8539
8540 #[test]
8541 fn validate_etiquetas_empty_takes_precedence_over_duplicate() {
8542 // Empty-first cascade pin: `("" "demo" "demo")` surfaces
8543 // `EtiquetaEmpty` not `EtiquetaDuplicate` — the narrower
8544 // structural "this entry has no value" defect dominates the
8545 // cross-entry uniqueness diagnostic. Mirrors the peer
8546 // empty-before-duplicate cascades on `:caracteristicas`
8547 // (`CaracteristicaEmpty` before `CaracteristicaDuplicate`,
8548 // fc3b4d5) and `:membros :caixa` (`MembroCaixaEmpty` before
8549 // `MembroDuplicate`).
8550 let c = caixa_with_etiquetas(vec!["", "demo", "demo"]);
8551 let err = c.validate_etiquetas().unwrap_err();
8552 assert!(matches!(err, ManifestError::EtiquetaEmpty), "got {err:?}",);
8553 }
8554
8555 #[test]
8556 fn validate_etiquetas_duplicate_reports_first_collision() {
8557 // First-collision pin: `("a" "b" "a" "b")` surfaces the `"a"`
8558 // duplicate (the lexicographically-earliest offending position
8559 // — the second `"a"` at index 2 collides with the first `"a"`
8560 // at index 0), not the later `"b"` collision at index 3,
8561 // peer with every other first-collision diagnostic posture on
8562 // this surface (`validate_load_singularity_reports_first_collision`,
8563 // `validate_cleanup_singularity_reports_first_collision`).
8564 let c = caixa_with_etiquetas(vec!["a", "b", "a", "b"]);
8565 let err = c.validate_etiquetas().unwrap_err();
8566 let ManifestError::EtiquetaDuplicate { etiqueta } = err else {
8567 panic!("expected EtiquetaDuplicate, got {err:?}");
8568 };
8569 assert_eq!(etiqueta, "a");
8570 }
8571
8572 #[test]
8573 fn validate_etiquetas_case_sensitive() {
8574 // Case-sensitivity pin: `("Foo" "foo")` is two distinct entries,
8575 // mirroring the peer `:membros :caixa` / `:children :caixa`
8576 // exact-string-match discipline. The shape gate this routine
8577 // landed (`is_chart_keyword_shape`, Cargo crates.io keyword
8578 // grammar) accepts mixed case — crates.io's keyword rule is
8579 // "case-insensitive" at the index layer but admits mixed case
8580 // at the entry layer (the canonical Helm chart `keywords:`
8581 // shape is lowercase by convention, but the grammar admits
8582 // uppercase). Case-sensitivity at the duplicate-set layer
8583 // remains structural — two distinct strings are two distinct
8584 // entries.
8585 let c = caixa_with_etiquetas(vec!["Foo", "foo"]);
8586 c.validate_etiquetas().unwrap();
8587 }
8588
8589 #[test]
8590 fn validate_etiquetas_diagnostic_carries_offending_tag() {
8591 // Diagnostic-shape pin (peer with
8592 // `validate_code_paths_diagnostic_carries_offending_slot_and_path`):
8593 // the error's Display surfaces the offending tag verbatim, so a
8594 // `feira lint` run can render the diagnostic without re-parsing
8595 // and the author can grep their caixa.lisp for the offending
8596 // value.
8597 let c = caixa_with_etiquetas(vec!["demo", "demo"]);
8598 let rendered = c.validate_etiquetas().unwrap_err().to_string();
8599 assert!(
8600 rendered.contains(":etiquetas"),
8601 "diagnostic must name the offending slot: {rendered}",
8602 );
8603 assert!(
8604 rendered.contains("demo"),
8605 "diagnostic must quote the offending tag: {rendered}",
8606 );
8607 }
8608
8609 #[test]
8610 fn validate_etiquetas_rejects_leading_whitespace_entry() {
8611 // Canonical paste-from-aligned-doc footgun. Without the shape
8612 // gate `" mesh"` silently passed validate and landed as a
8613 // YAML plain-style scalar with leading whitespace in the
8614 // rendered Chart.yaml `keywords:` array — every YAML 1.2
8615 // dumper trims leading whitespace from plain-style scalars,
8616 // so the authored space round-tripped inconsistently back
8617 // through `caixa.lisp`. Mirrors the peer
8618 // `validate_autores_rejects_leading_whitespace_entry`.
8619 let c = caixa_with_etiquetas(vec![" mesh"]);
8620 let err = c.validate_etiquetas().unwrap_err();
8621 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8622 panic!("expected EtiquetaInvalid, got {err:?}");
8623 };
8624 assert_eq!(etiqueta, " mesh");
8625 assert!(reason.contains("whitespace"), "got: {reason}");
8626 }
8627
8628 #[test]
8629 fn validate_etiquetas_rejects_embedded_newline_entry() {
8630 // Canonical paste-from-multiline-doc footgun — the author
8631 // pasted a multi-tag block into one `:etiquetas` entry
8632 // instead of splitting into one entry per tag. Without the
8633 // shape gate `"mesh\nhttp"` silently passed validate and
8634 // landed as a YAML-illegal multi-line scalar in the rendered
8635 // Chart.yaml `keywords:` array.
8636 let c = caixa_with_etiquetas(vec!["mesh\nhttp"]);
8637 let err = c.validate_etiquetas().unwrap_err();
8638 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8639 panic!("expected EtiquetaInvalid, got {err:?}");
8640 };
8641 assert_eq!(etiqueta, "mesh\nhttp");
8642 assert!(reason.contains("newline"), "got: {reason}");
8643 }
8644
8645 #[test]
8646 fn validate_etiquetas_rejects_embedded_comma_entry() {
8647 // Canonical CSV-list-separator-confusion footgun: the author
8648 // confused the CSV-style separator convention with the
8649 // `:etiquetas` list grammar. Without the shape gate
8650 // `"mesh,http,grpc"` silently passed validate and landed as a
8651 // single malformed search tag in the rendered Chart.yaml
8652 // `keywords:` array — Artifact Hub's keyword index would
8653 // either silently drop the tag or index it as
8654 // `mesh,http,grpc` instead of three separate tags.
8655 let c = caixa_with_etiquetas(vec!["mesh,http,grpc"]);
8656 let err = c.validate_etiquetas().unwrap_err();
8657 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8658 panic!("expected EtiquetaInvalid, got {err:?}");
8659 };
8660 assert_eq!(etiqueta, "mesh,http,grpc");
8661 assert!(reason.contains('`'), "got: {reason}");
8662 assert!(reason.contains(','), "got: {reason}");
8663 }
8664
8665 #[test]
8666 fn validate_etiquetas_rejects_embedded_slash_entry() {
8667 // Canonical path-separator-confusion footgun: the author
8668 // confused namespace-path notation with the keyword grammar.
8669 let c = caixa_with_etiquetas(vec!["caixa/servico"]);
8670 let err = c.validate_etiquetas().unwrap_err();
8671 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8672 panic!("expected EtiquetaInvalid, got {err:?}");
8673 };
8674 assert_eq!(etiqueta, "caixa/servico");
8675 assert!(reason.contains('/'), "got: {reason}");
8676 }
8677
8678 #[test]
8679 fn validate_etiquetas_rejects_leading_digit_entry() {
8680 // Canonical paste-from-numbered-list footgun: the author
8681 // copied `1. mesh` from a numbered doc and the `1` leaked
8682 // into the tag.
8683 let c = caixa_with_etiquetas(vec!["1mesh"]);
8684 let err = c.validate_etiquetas().unwrap_err();
8685 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8686 panic!("expected EtiquetaInvalid, got {err:?}");
8687 };
8688 assert_eq!(etiqueta, "1mesh");
8689 assert!(reason.contains("digit"), "got: {reason}");
8690 }
8691
8692 #[test]
8693 fn validate_etiquetas_rejects_leading_hyphen_entry() {
8694 // Canonical kebab-leak footgun.
8695 let c = caixa_with_etiquetas(vec!["-foo"]);
8696 let err = c.validate_etiquetas().unwrap_err();
8697 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8698 panic!("expected EtiquetaInvalid, got {err:?}");
8699 };
8700 assert_eq!(etiqueta, "-foo");
8701 assert!(reason.contains('-'), "got: {reason}");
8702 }
8703
8704 #[test]
8705 fn validate_etiquetas_rejects_non_ascii_entry() {
8706 // Canonical paste-from-Unicode-doc footgun. Every legitimate
8707 // search tag is strict ASCII; raw non-ASCII silently
8708 // round-trips inconsistently across NFC/NFD normalization on
8709 // APFS / case-folding filesystems and breaks the Artifact Hub
8710 // keyword search index lookup.
8711 let c = caixa_with_etiquetas(vec!["café"]);
8712 let err = c.validate_etiquetas().unwrap_err();
8713 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8714 panic!("expected EtiquetaInvalid, got {err:?}");
8715 };
8716 assert_eq!(etiqueta, "café");
8717 assert!(reason.contains("non-ASCII"), "got: {reason}");
8718 }
8719
8720 #[test]
8721 fn validate_etiquetas_rejects_period_entry() {
8722 // Canonical namespace-confusion / version-suffix footgun
8723 // (`"http.1"` / `"v1.0"`): Cargo's crates.io keyword grammar
8724 // excludes `.` from the continuation set even though the
8725 // sibling `:caracteristicas` axis (Cargo's feature-name
8726 // grammar) admits it. Tighter than the sibling axis, peer
8727 // with Cargo's own crates.io keyword shape.
8728 let c = caixa_with_etiquetas(vec!["http.1"]);
8729 let err = c.validate_etiquetas().unwrap_err();
8730 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8731 panic!("expected EtiquetaInvalid, got {err:?}");
8732 };
8733 assert_eq!(etiqueta, "http.1");
8734 assert!(reason.contains('.'), "got: {reason}");
8735 }
8736
8737 #[test]
8738 fn validate_etiquetas_empty_takes_precedence_over_shape() {
8739 // Per-entry empty-first cascade pin: an entry that is both
8740 // empty *and* shape-invalid surfaces `EtiquetaEmpty` (the
8741 // narrower "this entry has no value" structural defect
8742 // dominates the broader shape-predicate diagnostic). The
8743 // empty arm fires before the shape predicate is consulted,
8744 // mirroring the peer `validate_autores_empty_takes_precedence_over_shape`
8745 // cascade established on the sibling universal-axis Vec<String>
8746 // surface.
8747 let c = caixa_with_etiquetas(vec![""]);
8748 let err = c.validate_etiquetas().unwrap_err();
8749 assert!(matches!(err, ManifestError::EtiquetaEmpty), "got {err:?}",);
8750 }
8751
8752 #[test]
8753 fn validate_etiquetas_shape_takes_precedence_over_duplicate() {
8754 // Per-entry shape-before-cross-entry-duplicate cascade pin: an
8755 // entry that is malformed surfaces `EtiquetaInvalid` even when
8756 // a later entry would have collided on duplicate. The
8757 // per-entry shape arm fires inside the same loop iteration as
8758 // the empty arm, before the seen-set insert at end-of-iteration
8759 // — structural per-entry defects dominate the cross-entry
8760 // uniqueness diagnostic. Mirrors the peer
8761 // `validate_autores_shape_takes_precedence_over_duplicate`.
8762 let c = caixa_with_etiquetas(vec!["mesh\nhttp", "mesh\nhttp"]);
8763 let err = c.validate_etiquetas().unwrap_err();
8764 assert!(
8765 matches!(err, ManifestError::EtiquetaInvalid { .. }),
8766 "got {err:?}",
8767 );
8768 }
8769
8770 #[test]
8771 fn validate_etiquetas_invalid_diagnostic_names_offending_slot_and_value() {
8772 // Diagnostic-shape pin on the new shape arm (peer with
8773 // `validate_autores_invalid_diagnostic_names_offending_slot_and_value`):
8774 // the rendered Display surfaces both the offending slot name
8775 // and the offending value verbatim, so a `feira lint` run
8776 // points the author at the exact `:etiquetas` entry to fix.
8777 let c = caixa_with_etiquetas(vec!["mesh\nhttp"]);
8778 let rendered = c.validate_etiquetas().unwrap_err().to_string();
8779 assert!(
8780 rendered.contains(":etiquetas"),
8781 "diagnostic must name the offending slot: {rendered}",
8782 );
8783 assert!(
8784 rendered.contains("mesh\\nhttp"),
8785 "diagnostic must quote the offending value (debug-escaped): {rendered}",
8786 );
8787 }
8788
8789 #[test]
8790 fn validate_etiquetas_rejects_at_21_byte_boundary() {
8791 // The 20-byte cap pin — boundary-exceeding case rejected,
8792 // boundary-accepting case passes. Mirrors the peer
8793 // `chart_keyword_shape_rejects_at_21_byte_boundary` substrate-
8794 // side pin, surfaced at the per-axis caller so the cap
8795 // propagates through validate end-to-end. Constructed as a
8796 // single all-`a` token so only the cap arm fires.
8797 let max_ok = "a".repeat(20);
8798 let c = caixa_with_etiquetas(vec![max_ok.as_str()]);
8799 c.validate_etiquetas().unwrap();
8800 let too_long = "a".repeat(21);
8801 let c = caixa_with_etiquetas(vec![too_long.as_str()]);
8802 let err = c.validate_etiquetas().unwrap_err();
8803 let ManifestError::EtiquetaInvalid { reason, .. } = err else {
8804 panic!("expected EtiquetaInvalid, got {err:?}");
8805 };
8806 assert!(reason.contains("20"), "got: {reason}");
8807 assert!(reason.contains("21"), "got: {reason}");
8808 }
8809
8810 #[test]
8811 fn validate_etiquetas_accepts_canonical_shaped_forms() {
8812 // Positive control sweep: every canonical-shaped tag from the
8813 // hello-rio / checkout-aplicacao / pangea-tatara-akeyless
8814 // example fixtures plus the substrate-fixed tags caixa-helm
8815 // unions in at chart render. Drift between this list and the
8816 // substrate-side `chart_keyword_shape_accepts_canonical_forms`
8817 // sweep surfaces here — one source of truth for the rule.
8818 let c = caixa_with_etiquetas(vec![
8819 "example",
8820 "aplicacao",
8821 "mesh",
8822 "ecommerce",
8823 "demo",
8824 "infrastructure",
8825 "aws",
8826 "akeyless",
8827 "pangea-native",
8828 "hello-world",
8829 "wasm",
8830 "rust",
8831 "tatara-lisp",
8832 "caixa-servico",
8833 "lareira",
8834 ]);
8835 c.validate_etiquetas().unwrap();
8836 }
8837
8838 // ── validate_autores — universal-axis maintainer shape ────────────
8839
8840 fn caixa_with_autores(autores: Vec<&str>) -> Caixa {
8841 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
8842 c.autores = autores.into_iter().map(String::from).collect();
8843 c
8844 }
8845
8846 #[test]
8847 fn validate_autores_accepts_empty_list() {
8848 // The empty-list identity: `Caixa::template` emits `:autores ()`,
8849 // so the gate is non-disruptive against every existing manifest.
8850 let c = caixa_with_autores(vec![]);
8851 c.validate_autores().unwrap();
8852 }
8853
8854 #[test]
8855 fn validate_autores_accepts_canonical_forms() {
8856 // Positive control sweep: every canonical-shaped non-empty
8857 // distinct maintainer list passes — the hello-rio / checkout-
8858 // aplicacao fixtures' `:autores ("pleme-io")` shape, plus the
8859 // multi-author shape downstream packaging surfaces emit.
8860 let c = caixa_with_autores(vec!["pleme-io"]);
8861 c.validate_autores().unwrap();
8862 let c = caixa_with_autores(vec!["alice <alice@example.com>", "bob <bob@example.com>"]);
8863 c.validate_autores().unwrap();
8864 }
8865
8866 #[test]
8867 fn validate_autores_rejects_empty_entry() {
8868 // Canonical paste-from-blank-doc footgun. Without the gate the
8869 // empty entry rendered as `maintainers: [{name: "", email: null}]`
8870 // in `Chart.yaml`, a no-op maintainer the substrate cannot route
8871 // to.
8872 let c = caixa_with_autores(vec![""]);
8873 let err = c.validate_autores().unwrap_err();
8874 assert!(matches!(err, ManifestError::AutorEmpty), "got {err:?}",);
8875 }
8876
8877 #[test]
8878 fn validate_autores_rejects_duplicate_entry() {
8879 // Canonical copy-paste-the-wrong-author footgun. Unlike the
8880 // `:etiquetas` peer (caixa-helm's `BTreeSet` collect silently
8881 // dedups the rendered `keywords:` array), the `maintainers:`
8882 // rendering has *no* dedup — duplicates stack verbatim. The
8883 // duplicate-arm names the offending author verbatim.
8884 let c = caixa_with_autores(vec!["pleme-io", "pleme-io"]);
8885 let err = c.validate_autores().unwrap_err();
8886 let ManifestError::AutorDuplicate { autor } = err else {
8887 panic!("expected AutorDuplicate, got {err:?}");
8888 };
8889 assert_eq!(autor, "pleme-io");
8890 }
8891
8892 #[test]
8893 fn validate_autores_empty_takes_precedence_over_duplicate() {
8894 // Empty-first cascade pin: `("" "pleme-io" "pleme-io")` surfaces
8895 // `AutorEmpty` not `AutorDuplicate` — the narrower structural
8896 // "this entry has no value" defect dominates the cross-entry
8897 // uniqueness diagnostic. Mirrors the peer empty-before-duplicate
8898 // cascades on `:etiquetas` (`EtiquetaEmpty` before
8899 // `EtiquetaDuplicate`, 360a499), `:caracteristicas`
8900 // (`CaracteristicaEmpty` before `CaracteristicaDuplicate`,
8901 // fc3b4d5), and `:membros :caixa` (`MembroCaixaEmpty` before
8902 // `MembroDuplicate`).
8903 let c = caixa_with_autores(vec!["", "pleme-io", "pleme-io"]);
8904 let err = c.validate_autores().unwrap_err();
8905 assert!(matches!(err, ManifestError::AutorEmpty), "got {err:?}",);
8906 }
8907
8908 #[test]
8909 fn validate_autores_duplicate_reports_first_collision() {
8910 // First-collision pin: `("a" "b" "a" "b")` surfaces the `"a"`
8911 // duplicate (the lexicographically-earliest offending position
8912 // — the second `"a"` at index 2 collides with the first `"a"`
8913 // at index 0), not the later `"b"` collision at index 3,
8914 // peer with every other first-collision diagnostic posture on
8915 // this surface.
8916 let c = caixa_with_autores(vec!["a", "b", "a", "b"]);
8917 let err = c.validate_autores().unwrap_err();
8918 let ManifestError::AutorDuplicate { autor } = err else {
8919 panic!("expected AutorDuplicate, got {err:?}");
8920 };
8921 assert_eq!(autor, "a");
8922 }
8923
8924 #[test]
8925 fn validate_autores_case_sensitive() {
8926 // Case-sensitivity pin: `("Pleme-io" "pleme-io")` is two distinct
8927 // entries, mirroring the peer `:etiquetas` / `:membros :caixa`
8928 // / `:children :caixa` exact-string-match discipline.
8929 let c = caixa_with_autores(vec!["Pleme-io", "pleme-io"]);
8930 c.validate_autores().unwrap();
8931 }
8932
8933 #[test]
8934 fn validate_autores_diagnostic_carries_offending_author() {
8935 // Diagnostic-shape pin (peer with
8936 // `validate_etiquetas_diagnostic_carries_offending_tag`): the
8937 // error's Display surfaces the offending author verbatim, so a
8938 // `feira lint` run can render the diagnostic without re-parsing
8939 // and the author can grep their caixa.lisp for the offending
8940 // value.
8941 let c = caixa_with_autores(vec!["pleme-io", "pleme-io"]);
8942 let rendered = c.validate_autores().unwrap_err().to_string();
8943 assert!(
8944 rendered.contains(":autores"),
8945 "diagnostic must name the offending slot: {rendered}",
8946 );
8947 assert!(
8948 rendered.contains("pleme-io"),
8949 "diagnostic must quote the offending author: {rendered}",
8950 );
8951 }
8952
8953 #[test]
8954 fn validate_autores_rejects_leading_whitespace_entry() {
8955 // Canonical paste-from-aligned-doc footgun. Without the shape
8956 // gate `" pleme-io"` silently passed validate and landed as a
8957 // YAML plain-style scalar with leading whitespace in the
8958 // rendered Chart.yaml `maintainers:` array — every YAML 1.2
8959 // dumper trims leading whitespace from plain-style scalars, so
8960 // the authored space round-tripped inconsistently back through
8961 // `caixa.lisp`. Mirrors the peer
8962 // `validate_descricao_rejects_leading_whitespace`.
8963 let c = caixa_with_autores(vec![" pleme-io"]);
8964 let err = c.validate_autores().unwrap_err();
8965 let ManifestError::AutorInvalid { autor, reason } = err else {
8966 panic!("expected AutorInvalid, got {err:?}");
8967 };
8968 assert_eq!(autor, " pleme-io");
8969 assert!(reason.contains("whitespace"), "got: {reason}");
8970 }
8971
8972 #[test]
8973 fn validate_autores_rejects_trailing_whitespace_entry() {
8974 // Canonical paste-from-doc footgun.
8975 let c = caixa_with_autores(vec!["pleme-io "]);
8976 let err = c.validate_autores().unwrap_err();
8977 let ManifestError::AutorInvalid { autor, reason } = err else {
8978 panic!("expected AutorInvalid, got {err:?}");
8979 };
8980 assert_eq!(autor, "pleme-io ");
8981 assert!(reason.contains("whitespace"), "got: {reason}");
8982 }
8983
8984 #[test]
8985 fn validate_autores_rejects_embedded_newline_entry() {
8986 // Canonical paste-from-multiline-doc footgun — the author
8987 // pasted a multi-line block of author records into one
8988 // `:autores` entry instead of splitting into one entry per
8989 // author. Without the shape gate `"alice\nbob"` silently
8990 // passed validate and landed as a YAML-illegal multi-line
8991 // scalar in the rendered Chart.yaml `maintainers:` array.
8992 let c = caixa_with_autores(vec!["alice\nbob"]);
8993 let err = c.validate_autores().unwrap_err();
8994 let ManifestError::AutorInvalid { autor, reason } = err else {
8995 panic!("expected AutorInvalid, got {err:?}");
8996 };
8997 assert_eq!(autor, "alice\nbob");
8998 assert!(reason.contains("newline"), "got: {reason}");
8999 }
9000
9001 #[test]
9002 fn validate_autores_rejects_embedded_carriage_return_entry() {
9003 // Canonical paste-from-Windows-CRLF-doc footgun.
9004 let c = caixa_with_autores(vec!["alice\rbob"]);
9005 let err = c.validate_autores().unwrap_err();
9006 let ManifestError::AutorInvalid { autor, reason } = err else {
9007 panic!("expected AutorInvalid, got {err:?}");
9008 };
9009 assert_eq!(autor, "alice\rbob");
9010 assert!(reason.contains("carriage return"), "got: {reason}");
9011 }
9012
9013 #[test]
9014 fn validate_autores_rejects_embedded_tab_entry() {
9015 // Canonical tab-from-aligned-doc footgun.
9016 let c = caixa_with_autores(vec!["Pleme\tContributors"]);
9017 let err = c.validate_autores().unwrap_err();
9018 let ManifestError::AutorInvalid { autor, reason } = err else {
9019 panic!("expected AutorInvalid, got {err:?}");
9020 };
9021 assert_eq!(autor, "Pleme\tContributors");
9022 assert!(reason.contains("tab"), "got: {reason}");
9023 }
9024
9025 #[test]
9026 fn validate_autores_rejects_embedded_control_bytes_entry() {
9027 // Paste-from-binary-blob footguns: NUL, BEL, ESC, DEL all
9028 // surface the same control-byte arm.
9029 for entry in [
9030 "alice\x00bob",
9031 "alice\x07bob",
9032 "alice\x1bbob",
9033 "alice\x7fbob",
9034 ] {
9035 let c = caixa_with_autores(vec![entry]);
9036 let err = c.validate_autores().unwrap_err();
9037 let ManifestError::AutorInvalid { autor, reason } = err else {
9038 panic!("expected AutorInvalid for {entry:?}, got {err:?}");
9039 };
9040 assert_eq!(autor, entry);
9041 assert!(
9042 reason.contains("control character"),
9043 "{entry:?} reason: {reason}",
9044 );
9045 }
9046 }
9047
9048 #[test]
9049 fn validate_autores_accepts_unicode_entry() {
9050 // Unicode positive control: realistic maintainer names carry
9051 // Unicode (`François`, `日本語`, `naïve`). The predicate must
9052 // round-trip Unicode losslessly, peer with the
9053 // `chart_maintainer_name_shape_accepts_unicode` substrate-side
9054 // sweep.
9055 let c = caixa_with_autores(vec![
9056 "François Dupont",
9057 "日本語の名前",
9058 "naïve <naive@example.com>",
9059 ]);
9060 c.validate_autores().unwrap();
9061 }
9062
9063 #[test]
9064 fn validate_autores_empty_takes_precedence_over_shape() {
9065 // Per-entry empty-first cascade pin: an entry that is both
9066 // empty *and* shape-invalid surfaces `AutorEmpty` (the narrower
9067 // "this entry has no value" structural defect dominates the
9068 // broader shape-predicate diagnostic). The empty arm fires
9069 // before the shape predicate is consulted, mirroring the peer
9070 // `validate_repositorio_empty_takes_precedence_over_shape`
9071 // cascade on the universal `Option<String>` siblings — and now
9072 // established on the Vec<String> per-entry surface.
9073 let c = caixa_with_autores(vec![""]);
9074 let err = c.validate_autores().unwrap_err();
9075 assert!(matches!(err, ManifestError::AutorEmpty), "got {err:?}",);
9076 }
9077
9078 #[test]
9079 fn validate_autores_shape_takes_precedence_over_duplicate() {
9080 // Per-entry shape-before-cross-entry-duplicate cascade pin: an
9081 // entry that is malformed surfaces `AutorInvalid` even when a
9082 // later entry would have collided on duplicate. The per-entry
9083 // shape arm fires inside the same loop iteration as the empty
9084 // arm, before the seen-set insert at end-of-iteration —
9085 // structural per-entry defects dominate the cross-entry
9086 // uniqueness diagnostic.
9087 let c = caixa_with_autores(vec!["alice\nbob", "alice\nbob"]);
9088 let err = c.validate_autores().unwrap_err();
9089 assert!(
9090 matches!(err, ManifestError::AutorInvalid { .. }),
9091 "got {err:?}",
9092 );
9093 }
9094
9095 #[test]
9096 fn validate_autores_invalid_diagnostic_names_offending_slot_and_value() {
9097 // Diagnostic-shape pin on the new shape arm (peer with
9098 // `validate_descricao_invalid_diagnostic_carries_offending_value`):
9099 // the rendered Display surfaces both the offending slot name
9100 // and the offending value verbatim, so a `feira lint` run
9101 // points the author at the exact `:autores` entry to fix.
9102 let c = caixa_with_autores(vec!["alice\nbob"]);
9103 let rendered = c.validate_autores().unwrap_err().to_string();
9104 assert!(
9105 rendered.contains(":autores"),
9106 "diagnostic must name the offending slot: {rendered}",
9107 );
9108 assert!(
9109 rendered.contains("alice\\nbob"),
9110 "diagnostic must quote the offending value (debug-escaped): {rendered}",
9111 );
9112 }
9113
9114 #[test]
9115 fn validate_autores_rejects_at_129_byte_boundary() {
9116 // The 128-byte cap pin — boundary-exceeding case rejected,
9117 // boundary-accepting case passes. Mirrors the peer
9118 // `chart_maintainer_name_shape_rejects_at_129_byte_boundary`
9119 // substrate-side pin, surfaced at the per-axis caller so the
9120 // cap propagates through validate end-to-end. Constructed as
9121 // a single all-`a` token so only the cap arm fires.
9122 let max_ok = "a".repeat(128);
9123 let c = caixa_with_autores(vec![max_ok.as_str()]);
9124 c.validate_autores().unwrap();
9125 let too_long = "a".repeat(129);
9126 let c = caixa_with_autores(vec![too_long.as_str()]);
9127 let err = c.validate_autores().unwrap_err();
9128 let ManifestError::AutorInvalid { reason, .. } = err else {
9129 panic!("expected AutorInvalid, got {err:?}");
9130 };
9131 assert!(reason.contains("128"), "got: {reason}");
9132 assert!(reason.contains("129"), "got: {reason}");
9133 }
9134
9135 // ── validate_repositorio — universal-axis git-repo-URL shape ──────
9136
9137 fn caixa_with_repositorio(repositorio: Option<&str>) -> Caixa {
9138 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
9139 c.repositorio = repositorio.map(String::from);
9140 c
9141 }
9142
9143 #[test]
9144 fn validate_repositorio_accepts_none() {
9145 // The omit-the-slot identity: `:repositorio` is optional. The
9146 // gate is a no-op when the author didn't declare a value —
9147 // every caixa without a `:repositorio` line trivially passes,
9148 // and the substrate-side renderers fall back to their
9149 // documented placeholder (`caixa-helm`'s `home: None`,
9150 // `caixa-flux`'s `https://github.com/pleme-io/<nome>` derived
9151 // URL). Mirrors the peer `validate_restart_window_accepts_none`
9152 // posture on the other `Option<String>` Caixa slot.
9153 let c = caixa_with_repositorio(None);
9154 c.validate_repositorio().unwrap();
9155 }
9156
9157 #[test]
9158 fn validate_repositorio_accepts_canonical_forms() {
9159 // Positive control sweep across every documented `:repositorio`
9160 // authoring shape — the same union the shared
9161 // `crate::render::is_git_repo_url` predicate accepts and the
9162 // peer `:deps :fonte :repo` axis already routes through.
9163 // Covers the `github:` shorthand (the canonical pleme-io
9164 // convention used in the `:repositorio` field of every
9165 // manifest fixture across `caixa-helm` / `caixa-mesh` and the
9166 // `examples/`), the `https://…` URL the README quickstart uses,
9167 // the `ssh://`, `git://`, `git@host:path` scp-style SSH, and
9168 // `file://` URL schemes the shared predicate documents.
9169 for repo in [
9170 "github:pleme-io/hello-rio",
9171 "github:pleme-io/checkout",
9172 "https://github.com/pleme-io/hello-rio",
9173 "ssh://git@github.com/pleme-io/hello-rio.git",
9174 "git://github.com/pleme-io/hello-rio.git",
9175 "git@github.com:pleme-io/hello-rio.git",
9176 "file:///srv/pleme/hello-rio",
9177 ] {
9178 let c = caixa_with_repositorio(Some(repo));
9179 c.validate_repositorio()
9180 .unwrap_or_else(|err| panic!("canonical {repo:?} must pass: {err:?}"));
9181 }
9182 }
9183
9184 #[test]
9185 fn validate_repositorio_rejects_empty_some() {
9186 // Canonical paste-from-blank-doc footgun. The narrower
9187 // [`ManifestError::RepositorioEmpty`] arm fires before the
9188 // shape predicate is consulted, mirroring the empty-first
9189 // cascade every peer per-axis identity gate uses
9190 // (`NomeEmpty` → `NomeInvalid`, `VersaoEmpty` → `VersaoInvalid`,
9191 // `FonteRepoEmpty` → `FonteRepoInvalid`). Without this gate
9192 // the empty `Some("")` silently passed the renderer's
9193 // `Option::unwrap_or_else(|| <fallback>)` (which only fires
9194 // on `None`) and landed as `home: ""` in `Chart.yaml` /
9195 // `url: ""` in the FluxCD `GitRepository`.
9196 let c = caixa_with_repositorio(Some(""));
9197 let err = c.validate_repositorio().unwrap_err();
9198 assert!(
9199 matches!(err, ManifestError::RepositorioEmpty),
9200 "got {err:?}",
9201 );
9202 }
9203
9204 #[test]
9205 fn validate_repositorio_rejects_whitespace() {
9206 // Paste-from-doc whitespace footgun. The shared
9207 // `is_git_repo_url` predicate refuses any whitespace byte; a
9208 // trailing space in a `:repositorio` value silently broke
9209 // `git clone '<value> '` at clone time. The diagnostic names
9210 // the offending value verbatim.
9211 let c = caixa_with_repositorio(Some("github:pleme-io/hello-rio "));
9212 let err = c.validate_repositorio().unwrap_err();
9213 let ManifestError::RepositorioInvalid { repositorio, .. } = err else {
9214 panic!("expected RepositorioInvalid, got {err:?}");
9215 };
9216 assert_eq!(repositorio, "github:pleme-io/hello-rio ");
9217 }
9218
9219 #[test]
9220 fn validate_repositorio_rejects_control_char() {
9221 // Paste-from-multiline-doc CRLF footgun — control characters
9222 // at the URL boundary are a class of subprocess-arg injection
9223 // and break git's URL parser at every porcelain entry point.
9224 let c = caixa_with_repositorio(Some("https://example.com/repo\n"));
9225 let err = c.validate_repositorio().unwrap_err();
9226 assert!(
9227 matches!(err, ManifestError::RepositorioInvalid { .. }),
9228 "got {err:?}",
9229 );
9230 }
9231
9232 #[test]
9233 fn validate_repositorio_rejects_leading_dash() {
9234 // Canonical CLI-argument-injection footgun: `git clone <repo>`
9235 // interprets a leading `-` as a CLI flag, so a
9236 // `-upload-pack=…` value escapes the subprocess argument
9237 // boundary. The shared predicate refuses every leading-`-`
9238 // shape at validate time.
9239 let c = caixa_with_repositorio(Some("-upload-pack=evil"));
9240 let err = c.validate_repositorio().unwrap_err();
9241 assert!(
9242 matches!(err, ManifestError::RepositorioInvalid { .. }),
9243 "got {err:?}",
9244 );
9245 }
9246
9247 #[test]
9248 fn validate_repositorio_rejects_missing_colon_separator() {
9249 // The bare `org/repo` ambiguity footgun — `git clone` reads
9250 // a no-`:` form as a relative filesystem path rather than the
9251 // GitHub-shorthand expansion the author probably intended.
9252 // The shared predicate refuses every shape without a `:`
9253 // separator.
9254 let c = caixa_with_repositorio(Some("pleme-io/hello-rio"));
9255 let err = c.validate_repositorio().unwrap_err();
9256 assert!(
9257 matches!(err, ManifestError::RepositorioInvalid { .. }),
9258 "got {err:?}",
9259 );
9260 }
9261
9262 #[test]
9263 fn validate_repositorio_rejects_fragment_anchor() {
9264 // Paste-from-browser-address-bar footgun on the
9265 // `:repositorio` axis — an author copies a GitHub permalink
9266 // to a README section / line-permalink and forgets to trim
9267 // the `#fragment` tail. The shared `is_git_repo_url`
9268 // predicate refuses the byte at the URL-grammar layer
9269 // (libcurl strips the fragment before opening the
9270 // transport, so the byte rides verbatim into the rendered
9271 // `Chart.yaml` `home:` and FluxCD `GitRepository` `url:`
9272 // fields but is silently dropped on the wire — two
9273 // manifest variants whose values differ only in their
9274 // fragment anchor lock to two distinct rendered artifacts
9275 // for the byte-identical clone, defeating the THEORY.md
9276 // §V.2 render-determinism contract on the `:repositorio`
9277 // axis the peer `:fonte :repo` axis already closes).
9278 let c = caixa_with_repositorio(Some("https://github.com/pleme-io/hello-rio#readme"));
9279 let err = c.validate_repositorio().unwrap_err();
9280 let ManifestError::RepositorioInvalid {
9281 repositorio,
9282 reason,
9283 } = err
9284 else {
9285 panic!("expected RepositorioInvalid, got {err:?}");
9286 };
9287 assert_eq!(repositorio, "https://github.com/pleme-io/hello-rio#readme");
9288 assert!(
9289 reason.contains("must not contain `#`"),
9290 "reason must surface the fragment-`#` arm, got {reason:?}"
9291 );
9292 }
9293
9294 #[test]
9295 fn validate_repositorio_rejects_query_string() {
9296 // Paste-from-browser-address-bar footgun on the
9297 // `:repositorio` axis (peer with the a68f818 fragment-`#`
9298 // arm on the same axis). An author copies a GitHub tab
9299 // deep-link out of the address bar and forgets to trim
9300 // the `?tab=…` query tail. The shared `is_git_repo_url`
9301 // predicate refuses the byte at the URL-grammar layer
9302 // (GitHub / GitLab / Bitbucket silently ignore the
9303 // `?query` tail and serve the same repo regardless, so
9304 // the byte rides verbatim into the rendered `Chart.yaml`
9305 // `home:` and FluxCD `GitRepository` `url:` fields but
9306 // is silently masked at the wire — two manifest variants
9307 // whose values differ only in their query tail lock to
9308 // two distinct rendered artifacts for the byte-identical
9309 // clone, defeating the THEORY.md §V.2 render-determinism
9310 // contract on the `:repositorio` axis the peer `:fonte
9311 // :repo` axis already closes).
9312 let c = caixa_with_repositorio(Some(
9313 "https://github.com/pleme-io/hello-rio?tab=readme-ov-file",
9314 ));
9315 let err = c.validate_repositorio().unwrap_err();
9316 let ManifestError::RepositorioInvalid {
9317 repositorio,
9318 reason,
9319 } = err
9320 else {
9321 panic!("expected RepositorioInvalid, got {err:?}");
9322 };
9323 assert_eq!(
9324 repositorio,
9325 "https://github.com/pleme-io/hello-rio?tab=readme-ov-file"
9326 );
9327 assert!(
9328 reason.contains("must not contain `?`"),
9329 "reason must surface the query-`?` arm, got {reason:?}"
9330 );
9331 }
9332
9333 #[test]
9334 fn validate_repositorio_rejects_embedded_backslash() {
9335 // Windows-file-path-confusion footgun on the `:repositorio`
9336 // axis (peer with the prior fragment-`#` / query-`?` arms on
9337 // the same axis, and peer with the new dep-level `:fonte :repo`
9338 // backslash arm on the URL-grammar trajectory). An author
9339 // pastes a Windows Explorer address-bar `file:///C:\Users\me\
9340 // hello-rio` into the `:repositorio` slot, expecting the
9341 // `lareira-<nome>` chart's `home:` field and the FluxCD
9342 // `GitRepository` `url:` field to render the canonical local
9343 // file-URI. The shared `is_git_repo_url` predicate refuses
9344 // the byte at the URL-grammar layer (libcurl silently
9345 // translates `\` → `/` on some platforms and refuses it on
9346 // others, so the byte rides verbatim into the rendered
9347 // artifacts but is silently rewritten or rejected at the wire
9348 // — two manifest variants whose values differ only in
9349 // backslash-vs-forward-slash lock to two distinct rendered
9350 // artifacts for the byte-identical clone, defeating the
9351 // THEORY.md §V.2 render-determinism contract on the
9352 // `:repositorio` axis the peer `:fonte :repo` axis already
9353 // closes).
9354 let c = caixa_with_repositorio(Some("file:///C:\\Users\\me\\hello-rio"));
9355 let err = c.validate_repositorio().unwrap_err();
9356 let ManifestError::RepositorioInvalid {
9357 repositorio,
9358 reason,
9359 } = err
9360 else {
9361 panic!("expected RepositorioInvalid, got {err:?}");
9362 };
9363 assert_eq!(repositorio, "file:///C:\\Users\\me\\hello-rio");
9364 assert!(
9365 reason.contains("must not contain `\\`"),
9366 "reason must surface the backslash-`\\` arm, got {reason:?}"
9367 );
9368 }
9369
9370 #[test]
9371 fn validate_repositorio_rejects_uri_template_placeholder() {
9372 // URI Template (RFC 6570) placeholder footgun on the
9373 // `:repositorio` axis (peer with the prior fragment-`#` /
9374 // query-`?` / backslash-`\` arms on the same axis, and peer
9375 // with the new dep-level `:fonte :repo` `{` / `}` arm on the
9376 // URL-grammar trajectory). An author pastes a quick-start
9377 // README snippet / OpenAPI `servers:` URL / Helm chart
9378 // `home:` template carrying unresolved `{org}` / `{repo}`
9379 // placeholders into the `:repositorio` slot, expecting the
9380 // substrate to resolve the placeholder downstream. The
9381 // shared `is_git_repo_url` predicate refuses the byte at the
9382 // URL-grammar layer (libcurl percent-encodes `{` / `}` to
9383 // `%7B` / `%7D` on the wire, so the byte round-trips
9384 // inconsistently between the rendered `Chart.yaml home:` /
9385 // FluxCD `GitRepository url:` and the resolver's `git clone`
9386 // invocation, defeating the THEORY.md §V.2 render-
9387 // determinism contract on the `:repositorio` axis the peer
9388 // `:fonte :repo` axis already closes; every git porcelain
9389 // entry-point additionally fetches a nonexistent literal-
9390 // `{placeholder}`-named path far from the source caixa.lisp).
9391 let c = caixa_with_repositorio(Some("https://github.com/{org}/hello-rio"));
9392 let err = c.validate_repositorio().unwrap_err();
9393 let ManifestError::RepositorioInvalid {
9394 repositorio,
9395 reason,
9396 } = err
9397 else {
9398 panic!("expected RepositorioInvalid, got {err:?}");
9399 };
9400 assert_eq!(repositorio, "https://github.com/{org}/hello-rio");
9401 assert!(
9402 reason.contains("must not contain `{`"),
9403 "reason must surface the open-brace `{{` arm, got {reason:?}"
9404 );
9405 assert!(
9406 reason.contains("URI Template") || reason.contains("RFC 6570"),
9407 "reason must name the RFC 6570 URI Template grammar, got {reason:?}"
9408 );
9409 }
9410
9411 #[test]
9412 fn validate_repositorio_empty_takes_precedence_over_shape() {
9413 // Empty-first cascade pin: the empty `Some("")` surfaces the
9414 // narrower `RepositorioEmpty` not the shape-predicate-wrapped
9415 // `RepositorioInvalid`, mirroring the peer
9416 // `NomeEmpty` → `NomeInvalid`, `VersaoEmpty` → `VersaoInvalid`,
9417 // `FonteRepoEmpty` → `FonteRepoInvalid` cascades. The shared
9418 // `is_git_repo_url` predicate also rejects the empty input
9419 // (defensively, with its own `"must not be empty"` reason),
9420 // but the manifest-layer empty arm runs first to surface the
9421 // narrower diagnostic verbatim.
9422 let c = caixa_with_repositorio(Some(""));
9423 let err = c.validate_repositorio().unwrap_err();
9424 assert!(
9425 matches!(err, ManifestError::RepositorioEmpty),
9426 "got {err:?}",
9427 );
9428 }
9429
9430 #[test]
9431 fn validate_repositorio_diagnostic_carries_offending_value() {
9432 // Diagnostic-shape pin (peer with
9433 // `validate_autores_diagnostic_carries_offending_author`): the
9434 // error's Display surfaces the offending value + slot name
9435 // verbatim, so a `feira lint` run can render the diagnostic
9436 // without re-parsing and the author can grep their caixa.lisp
9437 // for the offending `:repositorio` value.
9438 let c = caixa_with_repositorio(Some("pleme-io/hello-rio"));
9439 let rendered = c.validate_repositorio().unwrap_err().to_string();
9440 assert!(
9441 rendered.contains(":repositorio"),
9442 "diagnostic must name the offending slot: {rendered}",
9443 );
9444 assert!(
9445 rendered.contains("pleme-io/hello-rio"),
9446 "diagnostic must quote the offending value: {rendered}",
9447 );
9448 }
9449
9450 // ── validate_descricao — universal-axis Chart.yaml description shape ──
9451
9452 fn caixa_with_descricao(descricao: Option<&str>) -> Caixa {
9453 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
9454 c.descricao = descricao.map(String::from);
9455 c
9456 }
9457
9458 #[test]
9459 fn validate_descricao_accepts_none() {
9460 // The omit-the-slot identity: `:descricao` is optional. The
9461 // gate is a no-op when the author didn't declare a value —
9462 // every caixa without a `:descricao` line trivially passes,
9463 // and the substrate-side renderers fall back to their
9464 // documented `caixa.nome`-derived placeholder. Mirrors the
9465 // peer `validate_repositorio_accepts_none` posture on the
9466 // sibling `Option<String>` Caixa slot.
9467 let c = caixa_with_descricao(None);
9468 c.validate_descricao().unwrap();
9469 }
9470
9471 #[test]
9472 fn validate_descricao_accepts_canonical_summary() {
9473 // Positive control: the canonical pleme-io descricao shape —
9474 // a short free-form prose summary — passes the gate. Covers
9475 // the fixture shapes the `caixa-helm` / `caixa-flux` /
9476 // `caixa-mesh` test fixtures use (`"Canonical Rust→wasm32-
9477 // wasip2 caixa Servico."`, `"Checkout flow."`).
9478 for desc in [
9479 "Canonical Rust→wasm32-wasip2 caixa Servico.",
9480 "Checkout flow.",
9481 "AWS provider caixa for tatara-lisp",
9482 "FIXME — describe this caixa",
9483 "x",
9484 ] {
9485 let c = caixa_with_descricao(Some(desc));
9486 c.validate_descricao()
9487 .unwrap_or_else(|err| panic!("canonical {desc:?} must pass: {err:?}"));
9488 }
9489 }
9490
9491 #[test]
9492 fn validate_descricao_rejects_empty_some() {
9493 // Canonical paste-from-blank-doc footgun. Without this gate
9494 // the empty `Some("")` silently passed the renderer's
9495 // `Option::unwrap_or_else(|| <fallback>)` (which only fires
9496 // on `None`) and landed as `description: ""` in `Chart.yaml`
9497 // and a blank `README.md` header. Mirrors the peer
9498 // [`ManifestError::RepositorioEmpty`] empty-arm on the
9499 // sibling `Option<String>` Caixa slot.
9500 let c = caixa_with_descricao(Some(""));
9501 let err = c.validate_descricao().unwrap_err();
9502 assert!(matches!(err, ManifestError::DescricaoEmpty), "got {err:?}",);
9503 }
9504
9505 #[test]
9506 fn validate_descricao_rejects_leading_whitespace() {
9507 // Paste-from-aligned-doc footgun: a leading ASCII space the
9508 // bare empty-arm gate accepted, the shape predicate now
9509 // refuses. The diagnostic carries the offending value
9510 // verbatim (with the leading space preserved) so the author
9511 // can grep their caixa.lisp for the exact `:descricao` line
9512 // and fix the round-trip-inconsistent leading whitespace.
9513 // Mirrors the peer
9514 // `validate_licenca_rejects_leading_whitespace` arm on the
9515 // sibling `:licenca` axis.
9516 let c = caixa_with_descricao(Some(" Checkout flow."));
9517 let err = c.validate_descricao().unwrap_err();
9518 let ManifestError::DescricaoInvalid { descricao, reason } = err else {
9519 panic!("expected DescricaoInvalid, got {err:?}");
9520 };
9521 assert_eq!(descricao, " Checkout flow.");
9522 assert!(reason.contains("whitespace"), "got: {reason:?}");
9523 }
9524
9525 #[test]
9526 fn validate_descricao_rejects_trailing_whitespace() {
9527 // Paste-from-doc footgun: a trailing ASCII space the bare
9528 // empty-arm gate accepted, the shape predicate now refuses.
9529 let c = caixa_with_descricao(Some("Checkout flow. "));
9530 let err = c.validate_descricao().unwrap_err();
9531 let ManifestError::DescricaoInvalid { descricao, reason } = err else {
9532 panic!("expected DescricaoInvalid, got {err:?}");
9533 };
9534 assert_eq!(descricao, "Checkout flow. ");
9535 assert!(reason.contains("whitespace"), "got: {reason:?}");
9536 }
9537
9538 #[test]
9539 fn validate_descricao_rejects_embedded_newline() {
9540 // Paste-from-multiline-doc footgun: an embedded LF the bare
9541 // empty-arm gate accepted, the shape predicate now refuses.
9542 // Without this gate the embedded newline silently landed in
9543 // the rendered Chart.yaml as a multi-line YAML block scalar,
9544 // and every chart-aware UI (`helm list`, `helm search`,
9545 // Artifact Hub) renders the description in a single-line
9546 // column so the embedded newline is silently dropped at
9547 // every downstream consumer.
9548 let c = caixa_with_descricao(Some("Checkout\nflow."));
9549 let err = c.validate_descricao().unwrap_err();
9550 assert!(
9551 matches!(err, ManifestError::DescricaoInvalid { .. }),
9552 "got {err:?}",
9553 );
9554 assert!(err.to_string().contains("newline"), "got {err}");
9555 }
9556
9557 #[test]
9558 fn validate_descricao_rejects_embedded_carriage_return() {
9559 // Paste-from-Windows-CRLF-doc footgun.
9560 let c = caixa_with_descricao(Some("Checkout\rflow."));
9561 let err = c.validate_descricao().unwrap_err();
9562 assert!(
9563 matches!(err, ManifestError::DescricaoInvalid { .. }),
9564 "got {err:?}",
9565 );
9566 assert!(err.to_string().contains("carriage return"), "got {err}");
9567 }
9568
9569 #[test]
9570 fn validate_descricao_rejects_embedded_tab() {
9571 // Tab-from-aligned-doc footgun.
9572 let c = caixa_with_descricao(Some("Checkout\tflow."));
9573 let err = c.validate_descricao().unwrap_err();
9574 assert!(
9575 matches!(err, ManifestError::DescricaoInvalid { .. }),
9576 "got {err:?}",
9577 );
9578 assert!(err.to_string().contains("tab"), "got {err}");
9579 }
9580
9581 #[test]
9582 fn validate_descricao_rejects_embedded_control_bytes() {
9583 // Paste-from-binary-blob footgun: every other control byte
9584 // (NUL, BEL, ESC, DEL) is refused at validate time. Mirrors
9585 // the peer SPDX-expression control-byte arm.
9586 for s in [
9587 "Checkout\x00flow.",
9588 "Checkout\x07flow.",
9589 "Checkout\x1bflow.",
9590 "Checkout\x7fflow.",
9591 ] {
9592 let c = caixa_with_descricao(Some(s));
9593 let err = c.validate_descricao().unwrap_err();
9594 assert!(
9595 matches!(err, ManifestError::DescricaoInvalid { .. }),
9596 "{s:?} got {err:?}",
9597 );
9598 assert!(
9599 err.to_string().contains("control character"),
9600 "{s:?} got {err}",
9601 );
9602 }
9603 }
9604
9605 #[test]
9606 fn validate_descricao_accepts_unicode_prose() {
9607 // Positive control: Unicode prose is accepted — the
9608 // canonical fixtures carry `→` (U+2192) and `—` (U+2014),
9609 // and `Caixa::template`'s `"FIXME — describe this caixa"`
9610 // scaffold every `feira init` emits must continue to pass.
9611 for s in [
9612 "Canonical Rust→wasm32-wasip2 caixa Servico.",
9613 "FIXME — describe this caixa",
9614 "Caixa pour le projet tâche",
9615 "日本語の説明",
9616 ] {
9617 let c = caixa_with_descricao(Some(s));
9618 c.validate_descricao()
9619 .unwrap_or_else(|err| panic!("Unicode {s:?} must pass: {err:?}"));
9620 }
9621 }
9622
9623 #[test]
9624 fn validate_descricao_empty_takes_precedence_over_shape() {
9625 // Cascade pin: a `Some("")` surfaces the narrower
9626 // `DescricaoEmpty` arm, not the broader `DescricaoInvalid`
9627 // shape-predicate arm. Mirrors the peer
9628 // `validate_licenca_empty_takes_precedence_over_shape` pin
9629 // on the sibling `:licenca` axis.
9630 let c = caixa_with_descricao(Some(""));
9631 let err = c.validate_descricao().unwrap_err();
9632 assert!(matches!(err, ManifestError::DescricaoEmpty), "got {err:?}",);
9633 }
9634
9635 #[test]
9636 fn validate_descricao_invalid_diagnostic_carries_offending_value_and_slot() {
9637 // Diagnostic-shape pin: the error's Display surfaces both
9638 // the `:descricao` slot name and the offending value
9639 // verbatim, so a `feira lint` run can render the diagnostic
9640 // without re-parsing and the author can grep their caixa.lisp
9641 // for the offending `:descricao` line. Mirrors the peer
9642 // `validate_licenca_invalid_diagnostic_carries_offending_value_and_slot`
9643 // pin (ee2e888) on the sibling `:licenca` axis.
9644 // The `{descricao:?}` Debug format escapes embedded control
9645 // bytes; the quoted offending value surfaces as
9646 // `"Checkout\nflow."` (literal backslash-n) in the rendered
9647 // diagnostic. The author can grep their caixa.lisp for the
9648 // literal `Checkout` summary prefix.
9649 let c = caixa_with_descricao(Some("Checkout\nflow."));
9650 let rendered = c.validate_descricao().unwrap_err().to_string();
9651 assert!(
9652 rendered.contains(":descricao"),
9653 "diagnostic must name the offending slot: {rendered}",
9654 );
9655 assert!(
9656 rendered.contains("Checkout\\nflow."),
9657 "diagnostic must quote the offending value (debug-escaped): {rendered}",
9658 );
9659 }
9660
9661 #[test]
9662 fn validate_descricao_template_passes() {
9663 // Round-trip pin: the bare `Caixa::template` shape carries
9664 // `:descricao "FIXME — describe this caixa"` (a non-empty
9665 // sentinel), so the template-derived Caixa passes the gate by
9666 // construction. A future template-shape change that omits or
9667 // empties `:descricao` would surface here as a regression.
9668 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
9669 c.validate_descricao().unwrap();
9670 }
9671
9672 #[test]
9673 fn validate_descricao_diagnostic_names_offending_slot() {
9674 // Diagnostic-shape pin (peer with
9675 // `validate_repositorio_diagnostic_carries_offending_value`):
9676 // the error's Display surfaces the `:descricao` slot name
9677 // verbatim, so a `feira lint` run can render the diagnostic
9678 // without re-parsing and the author can grep their caixa.lisp
9679 // for the offending `:descricao` line.
9680 let c = caixa_with_descricao(Some(""));
9681 let rendered = c.validate_descricao().unwrap_err().to_string();
9682 assert!(
9683 rendered.contains(":descricao"),
9684 "diagnostic must name the offending slot: {rendered}",
9685 );
9686 }
9687
9688 // ── validate_licenca — universal-axis chart README license shape ──
9689
9690 fn caixa_with_licenca(licenca: Option<&str>) -> Caixa {
9691 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
9692 c.licenca = licenca.map(String::from);
9693 c
9694 }
9695
9696 #[test]
9697 fn validate_licenca_accepts_none() {
9698 // The omit-the-slot identity: `:licenca` is optional. The
9699 // gate is a no-op when the author didn't declare a value —
9700 // every caixa without a `:licenca` line trivially passes,
9701 // and the substrate-side `caixa-helm` renderer falls back to
9702 // the documented `"MIT"` placeholder. Mirrors the peer
9703 // `validate_descricao_accepts_none` posture on the sibling
9704 // `Option<String>` Caixa slot.
9705 let c = caixa_with_licenca(None);
9706 c.validate_licenca().unwrap();
9707 }
9708
9709 #[test]
9710 fn validate_licenca_accepts_canonical_expressions() {
9711 // Positive control: every canonical SPDX expression shape
9712 // pleme-io carries in its existing fixtures + the canonical
9713 // SPDX dual-license / with-exception / `+`-suffix / grouped /
9714 // user-defined-reference shapes all pass the gate. Covers
9715 // the single-license, `OR`-compound, `AND`-compound,
9716 // `WITH`-exception, parenthesis-grouped, `+`-suffix, and
9717 // `LicenseRef-` / `DocumentRef-:LicenseRef-` shapes — every
9718 // production the SPDX 2.1 expression grammar admits that
9719 // sits within the alphabet floor the
9720 // `is_spdx_expression_shape` predicate enforces.
9721 for lic in [
9722 "MIT",
9723 "Apache-2.0",
9724 "Apache-2.0 OR MIT",
9725 "Apache-2.0 AND MIT",
9726 "BSD-3-Clause",
9727 "MPL-2.0",
9728 "GPL-3.0-or-later",
9729 "GPL-2.0+",
9730 "Apache-2.0 WITH LLVM-exception",
9731 "(MIT OR Apache-2.0) AND BSD-3-Clause",
9732 "(MIT OR Apache-2.0) AND BSD-3-Clause AND ISC",
9733 "LicenseRef-MyLicense",
9734 "DocumentRef-spdx-tool:LicenseRef-MIT-Style",
9735 "x",
9736 ] {
9737 let c = caixa_with_licenca(Some(lic));
9738 c.validate_licenca()
9739 .unwrap_or_else(|err| panic!("canonical {lic:?} must pass: {err:?}"));
9740 }
9741 }
9742
9743 #[test]
9744 fn validate_licenca_rejects_trailing_whitespace() {
9745 // Paste-from-doc whitespace footgun. A trailing space in the
9746 // `:licenca` value would silently break a downstream SPDX
9747 // parser that splits on exact `AND` / `OR` / `WITH` keyword
9748 // boundaries. The shape predicate refuses every trailing
9749 // whitespace byte by construction. Peer with
9750 // `validate_repositorio_rejects_whitespace` and
9751 // `validate_edicao_rejects_trailing_whitespace`.
9752 let c = caixa_with_licenca(Some("MIT "));
9753 let err = c.validate_licenca().unwrap_err();
9754 let ManifestError::LicencaInvalid { licenca, .. } = err else {
9755 panic!("expected LicencaInvalid, got {err:?}");
9756 };
9757 assert_eq!(licenca, "MIT ");
9758 }
9759
9760 #[test]
9761 fn validate_licenca_rejects_leading_whitespace() {
9762 // Symmetric paste-from-doc whitespace footgun on the leading
9763 // boundary — the gate refuses every shape that starts with a
9764 // space byte by construction. Peer with
9765 // `validate_edicao_rejects_leading_whitespace`.
9766 let c = caixa_with_licenca(Some(" MIT"));
9767 let err = c.validate_licenca().unwrap_err();
9768 assert!(
9769 matches!(err, ManifestError::LicencaInvalid { .. }),
9770 "got {err:?}",
9771 );
9772 }
9773
9774 #[test]
9775 fn validate_licenca_rejects_control_char() {
9776 // Paste-from-multiline-doc CRLF footgun — control characters
9777 // at the value boundary land as a malformed line in the
9778 // rendered chart `README.md` `## License` section. Peer with
9779 // `validate_repositorio_rejects_control_char` and
9780 // `validate_edicao_rejects_control_char`.
9781 for lic in ["MIT\n", "MIT\r\n", "MIT\rApache-2.0"] {
9782 let c = caixa_with_licenca(Some(lic));
9783 let err = c.validate_licenca().unwrap_err();
9784 assert!(
9785 matches!(err, ManifestError::LicencaInvalid { .. }),
9786 "expected LicencaInvalid on {lic:?}, got {err:?}",
9787 );
9788 }
9789 }
9790
9791 #[test]
9792 fn validate_licenca_rejects_tab() {
9793 // Tab-from-aligned-doc footgun — SPDX expressions use a
9794 // single ASCII space between tokens; a tab breaks every
9795 // downstream SPDX parser that splits on exact `" "`
9796 // boundaries.
9797 let c = caixa_with_licenca(Some("MIT\tOR Apache-2.0"));
9798 let err = c.validate_licenca().unwrap_err();
9799 assert!(
9800 matches!(err, ManifestError::LicencaInvalid { .. }),
9801 "got {err:?}",
9802 );
9803 }
9804
9805 #[test]
9806 fn validate_licenca_rejects_non_ascii() {
9807 // Smart-quote / non-ASCII paste footgun — SPDX identifiers
9808 // are ASCII per the `idstring = 1*(ALPHA / DIGIT / "-" /
9809 // ".")` production. The shape predicate refuses every
9810 // non-ASCII byte by construction; peer with
9811 // `validate_edicao_rejects_non_ascii_lookalike`.
9812 for lic in ["MIT\u{a0}OR Apache-2.0", "MIT\u{2013}1.0", "Café-1.0"] {
9813 let c = caixa_with_licenca(Some(lic));
9814 let err = c.validate_licenca().unwrap_err();
9815 assert!(
9816 matches!(err, ManifestError::LicencaInvalid { .. }),
9817 "expected LicencaInvalid on {lic:?}, got {err:?}",
9818 );
9819 }
9820 }
9821
9822 #[test]
9823 fn validate_licenca_rejects_underscore() {
9824 // Underscore-instead-of-hyphen typo footgun — `Apache_2.0` /
9825 // `MIT_Style` / `BSD_3_Clause` are familiar shapes from
9826 // snake-case identifier conventions that don't apply to the
9827 // SPDX `idstring` grammar (which admits only `ALPHA / DIGIT /
9828 // "-" / "."`). The shape predicate refuses every underscore
9829 // byte by construction.
9830 for lic in ["Apache_2.0", "MIT_Style", "BSD_3_Clause"] {
9831 let c = caixa_with_licenca(Some(lic));
9832 let err = c.validate_licenca().unwrap_err();
9833 assert!(
9834 matches!(err, ManifestError::LicencaInvalid { .. }),
9835 "expected LicencaInvalid on {lic:?}, got {err:?}",
9836 );
9837 }
9838 }
9839
9840 #[test]
9841 fn validate_licenca_rejects_comma_separator() {
9842 // Comma-instead-of-`OR`-keyword colloquial idiom footgun —
9843 // SPDX expressions compose multiple licenses via `AND` / `OR`
9844 // keywords, not the comma separator. The shape predicate
9845 // refuses every comma byte by construction.
9846 for lic in ["MIT, Apache-2.0", "MIT,Apache-2.0"] {
9847 let c = caixa_with_licenca(Some(lic));
9848 let err = c.validate_licenca().unwrap_err();
9849 assert!(
9850 matches!(err, ManifestError::LicencaInvalid { .. }),
9851 "expected LicencaInvalid on {lic:?}, got {err:?}",
9852 );
9853 }
9854 }
9855
9856 #[test]
9857 fn validate_licenca_rejects_slash_dual_license() {
9858 // Slash-dual-license colloquial idiom footgun — the
9859 // `MIT/Apache-2.0` shape is common in Cargo's pre-SPDX
9860 // `package.license` field but non-SPDX; the SPDX equivalent
9861 // is `MIT OR Apache-2.0`. The shape predicate refuses every
9862 // forward-slash byte by construction.
9863 for lic in ["MIT/Apache-2.0", "MIT/BSD-3-Clause"] {
9864 let c = caixa_with_licenca(Some(lic));
9865 let err = c.validate_licenca().unwrap_err();
9866 assert!(
9867 matches!(err, ManifestError::LicencaInvalid { .. }),
9868 "expected LicencaInvalid on {lic:?}, got {err:?}",
9869 );
9870 }
9871 }
9872
9873 #[test]
9874 fn validate_licenca_rejects_semicolon_separator() {
9875 // Semicolon-list-separator confusion footgun — adjacent to
9876 // the comma-separator idiom, every list-separator-belongs-
9877 // to-list-grammar confusion lands here.
9878 let c = caixa_with_licenca(Some("MIT; Apache-2.0"));
9879 let err = c.validate_licenca().unwrap_err();
9880 assert!(
9881 matches!(err, ManifestError::LicencaInvalid { .. }),
9882 "got {err:?}",
9883 );
9884 }
9885
9886 #[test]
9887 fn validate_licenca_empty_takes_precedence_over_shape() {
9888 // Empty-first cascade pin: the empty `Some("")` surfaces the
9889 // narrower `LicencaEmpty` not the shape-predicate-wrapped
9890 // `LicencaInvalid`, mirroring the peer
9891 // `validate_edicao_empty_takes_precedence_over_shape` and
9892 // `validate_repositorio_empty_takes_precedence_over_shape`
9893 // (`RepositorioEmpty` → `RepositorioInvalid`), `NomeEmpty` →
9894 // `NomeInvalid`, `VersaoEmpty` → `VersaoInvalid` cascades.
9895 // The shape predicate also refuses the empty input
9896 // (defensively — `"must not be empty"`), but the manifest-
9897 // layer empty arm runs first to surface the narrower
9898 // diagnostic verbatim.
9899 let c = caixa_with_licenca(Some(""));
9900 let err = c.validate_licenca().unwrap_err();
9901 assert!(matches!(err, ManifestError::LicencaEmpty), "got {err:?}",);
9902 }
9903
9904 #[test]
9905 fn validate_licenca_invalid_diagnostic_carries_offending_value() {
9906 // Diagnostic-shape pin on the shape-predicate arm (peer with
9907 // `validate_edicao_invalid_diagnostic_carries_offending_value`
9908 // and `validate_repositorio_diagnostic_carries_offending_value`):
9909 // the error's Display surfaces the offending value + slot
9910 // name verbatim, so a `feira lint` run can render the
9911 // diagnostic without re-parsing and the author can grep
9912 // their caixa.lisp for the offending `:licenca` value.
9913 let c = caixa_with_licenca(Some("Apache_2.0"));
9914 let rendered = c.validate_licenca().unwrap_err().to_string();
9915 assert!(
9916 rendered.contains(":licenca"),
9917 "diagnostic must name the offending slot: {rendered}",
9918 );
9919 assert!(
9920 rendered.contains("Apache_2.0"),
9921 "diagnostic must quote the offending value: {rendered}",
9922 );
9923 }
9924
9925 #[test]
9926 fn validate_licenca_rejects_empty_some() {
9927 // Canonical paste-from-blank-doc footgun. Without this gate
9928 // the empty `Some("")` silently passed the renderer's
9929 // `Option::unwrap_or_else(|| "MIT".into())` (which only
9930 // fires on `None`) and landed as a bare trailing period in
9931 // the rendered chart `README.md` `## License` section.
9932 // Mirrors the peer [`ManifestError::DescricaoEmpty`] empty-
9933 // arm on the sibling `Option<String>` Caixa slot.
9934 let c = caixa_with_licenca(Some(""));
9935 let err = c.validate_licenca().unwrap_err();
9936 assert!(matches!(err, ManifestError::LicencaEmpty), "got {err:?}",);
9937 }
9938
9939 #[test]
9940 fn validate_licenca_template_passes() {
9941 // Round-trip pin: the bare `Caixa::template` shape (whether
9942 // it carries `:licenca` or omits it) passes the gate by
9943 // construction. A future template-shape change that
9944 // introduced `(:licenca "")` would surface here as a
9945 // regression. Mirrors the peer
9946 // `validate_descricao_template_passes` pin.
9947 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
9948 c.validate_licenca().unwrap();
9949 }
9950
9951 #[test]
9952 fn validate_licenca_diagnostic_names_offending_slot() {
9953 // Diagnostic-shape pin (peer with
9954 // `validate_descricao_diagnostic_names_offending_slot`):
9955 // the error's Display surfaces the `:licenca` slot name
9956 // verbatim, so a `feira lint` run can render the diagnostic
9957 // without re-parsing and the author can grep their caixa.lisp
9958 // for the offending `:licenca` line.
9959 let c = caixa_with_licenca(Some(""));
9960 let rendered = c.validate_licenca().unwrap_err().to_string();
9961 assert!(
9962 rendered.contains(":licenca"),
9963 "diagnostic must name the offending slot: {rendered}",
9964 );
9965 }
9966
9967 // ── Caixa::licenca — outer top-level Option<&str> scalar accessor ──
9968
9969 #[test]
9970 fn licenca_returns_licenca_byte_string_verbatim_across_permutations() {
9971 // The canonical per-`Caixa` `:licenca` SPDX-expression scalar
9972 // pin: [`Caixa::licenca`] must return the `:licenca` typed
9973 // byte-string verbatim as an `Option<&str>`, byte-equal to the
9974 // raw `self.licenca.as_deref()` access across every
9975 // representative value in the accept-set — `None` (the "omit
9976 // the slot to defer to the caixa-helm renderer's `MIT`
9977 // fallback" arm every existing fixture without a `:licenca`
9978 // line carries), `Some("")` (a past-the-guard sentinel that
9979 // pins the accessor doesn't perform a silent
9980 // `Some("") → None` collapse on the empty arm — validate
9981 // rejects `Some("")` through `LicencaEmpty` but the accessor
9982 // must ship the raw slot verbatim so a validate-time gate
9983 // regression surfaces at the caixa-helm emit boundary rather
9984 // than being silently absorbed into the fallback), `Some("MIT")`
9985 // (the canonical single-license shape every `feira init`
9986 // template scaffolds), `Some("Apache-2.0 OR MIT")` (the
9987 // canonical `OR`-compound shape the peer
9988 // `validate_licenca_accepts_canonical_expressions` positive
9989 // sweep exercises), `Some("(MIT OR Apache-2.0) AND
9990 // BSD-3-Clause")` (the canonical parenthesis-grouped shape),
9991 // `Some("MIT ")` / `Some(" MIT")` / `Some("MIT\n")` /
9992 // `Some("Apache_2.0")` / `Some("MIT,Apache-2.0")` (past-the-
9993 // guard sentinels — validate rejects each through
9994 // `LicencaInvalid` but the accessor must ship the raw slot
9995 // verbatim).
9996 //
9997 // First outer top-level [`Caixa`] `Option<&str>`-return scalar
9998 // accessor pin on the substrate primitive — opens the "outer
9999 // [`Caixa`] `Option<&str>` scalar" projection pattern the
10000 // sibling per-`Caixa` `:descricao` / `:repositorio` / `:edicao`
10001 // future lifts fold on. Sibling in shape to the peer per-`:placement`
10002 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
10003 // [`crate::aplicacao::Placement::affinity`] (74ec2d3) accessor
10004 // pins on the sibling per-M3-mesh-slot `Option<&str>`-return
10005 // axes, extended onto the outer top-level [`Caixa`] universal-
10006 // axis surface. Pins against a future silent detour that
10007 // returned an owned `Option<String>` (which would type-check
10008 // but silently allocate on every accessor call, breaking the
10009 // zero-cost projection every peer sibling accessor carries), a
10010 // `Some("") → None` collapse (which would silently absorb the
10011 // `LicencaEmpty` refusal case at the accessor boundary and the
10012 // caixa-helm emit path would silently fall back to `"MIT"` on
10013 // a struct-literal `Caixa { licenca: Some(""), .. }`), or a
10014 // `None → Some("MIT")` collapse (which would silently reify
10015 // the caixa-helm renderer's `"MIT"` fallback at the accessor
10016 // boundary and every downstream consumer keying off the
10017 // `Option::is_none()` discriminator would lose the "author
10018 // omitted the slot" signal).
10019 for licenca in [
10020 None,
10021 Some(""),
10022 Some("MIT"),
10023 Some("Apache-2.0 OR MIT"),
10024 Some("(MIT OR Apache-2.0) AND BSD-3-Clause"),
10025 Some("MIT "),
10026 Some(" MIT"),
10027 Some("MIT\n"),
10028 Some("Apache_2.0"),
10029 Some("MIT,Apache-2.0"),
10030 ] {
10031 let c = caixa_with_licenca(licenca);
10032 assert_eq!(
10033 c.licenca(),
10034 licenca,
10035 "Caixa::licenca must return :licenca verbatim (got {:?}, \
10036 expected {licenca:?})",
10037 c.licenca(),
10038 );
10039 assert_eq!(
10040 c.licenca(),
10041 c.licenca.as_deref(),
10042 "Caixa::licenca must byte-equal the raw \
10043 `self.licenca.as_deref()` field access across every \
10044 value in the Option<&str> accept-set",
10045 );
10046 }
10047 }
10048
10049 #[test]
10050 fn validate_licenca_empty_arm_routes_through_accessor() {
10051 // Composition pin: [`Caixa::validate_licenca`]'s empty-arm gate
10052 // must key off [`Caixa::licenca`], not the raw
10053 // `self.licenca.as_deref()` field access. Structurally: a
10054 // `Caixa { licenca: Some(""), .. }` must surface the
10055 // `LicencaEmpty` refusal exactly, and a
10056 // `Caixa { licenca: Some("MIT"), .. }` (the canonical
10057 // single-license form) must pass validate. The pair jointly
10058 // pins the accessor + validate-gate composition: any future
10059 // silent detour that had the accessor return `None` on the
10060 // empty arm (a `.filter(|s| !s.is_empty())` collapse) would
10061 // silently absorb the `LicencaEmpty` refusal at the accessor
10062 // boundary and the validate gate would accept a struct-literal
10063 // `Caixa { licenca: Some(""), .. }` — the composition pin
10064 // catches that at caixa-core build time.
10065 //
10066 // Peer of the per-`:politicas :circuit-breaker`
10067 // [`crate::aplicacao::CircuitBreaker::max_failures`] (3a74062)
10068 // accessor-composition pin
10069 // (`validate_politicas_max_failures_zero_floor_arm_routes_through_accessor`)
10070 // on the sibling per-M3-mesh-slot required-`u32` axis — same
10071 // "the validate / shape-gate predicate must route through the
10072 // substrate-primitive typed dispatch" discipline extended onto
10073 // the outer top-level [`Caixa`] universal-axis
10074 // `Option<&str>`-composition surface.
10075 let c = caixa_with_licenca(Some(""));
10076 assert!(
10077 matches!(c.validate_licenca(), Err(ManifestError::LicencaEmpty)),
10078 "validate_licenca must reject licenca == Some(\"\") with \
10079 LicencaEmpty — the accessor and the validate gate must \
10080 route through the same substrate-primitive typed dispatch \
10081 on the :licenca empty arm",
10082 );
10083 let c = caixa_with_licenca(Some("MIT"));
10084 assert!(
10085 c.validate_licenca().is_ok(),
10086 "validate_licenca must accept licenca == Some(\"MIT\") \
10087 (the canonical single-license SPDX shape)",
10088 );
10089 }
10090
10091 #[test]
10092 fn licenca_projects_option_str_by_borrow() {
10093 // The by-borrow pin: [`Caixa::licenca`] returns
10094 // `Option<&str>` by borrow — the `&str` borrows the underlying
10095 // `String` storage of the `Option<String>` slot and the
10096 // accessor must not allocate a fresh `String` on every call.
10097 // Peer of the per-`:placement`
10098 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) by-
10099 // borrow pin on the peer per-M3-mesh-slot
10100 // `Option<&str>`-return axis, extended onto the outer top-
10101 // level [`Caixa`] universal-axis `Option<&str>` shape — the
10102 // accessor's returned `&str` must borrow from `&self` (the
10103 // returned reference's lifetime is tied to `&self`), and
10104 // calling the accessor twice on the same [`Caixa`] must yield
10105 // the same `Option<&str>` verbatim (idempotent, no side
10106 // effects on `&self`).
10107 //
10108 // Pins against a future silent detour that returned an owned
10109 // `Option<String>` (which would type-check but silently
10110 // allocate on every call, breaking the zero-cost projection
10111 // every peer sibling accessor carries), or a one-arm-only
10112 // accessor that returned a saturating value on some sentinel
10113 // input (breaking the pass-through invariant the sibling
10114 // required-scalar accessors carry).
10115 for licenca in [None, Some(""), Some("MIT"), Some("Apache-2.0 OR MIT")] {
10116 let c = caixa_with_licenca(licenca);
10117 let first = c.licenca();
10118 let second = c.licenca();
10119 assert_eq!(
10120 first, second,
10121 "Caixa::licenca must be idempotent — two successive \
10122 calls on the same &self must return the same \
10123 Option<&str>",
10124 );
10125 assert_eq!(
10126 first, licenca,
10127 "Caixa::licenca must return :licenca verbatim by \
10128 borrow — got {first:?}, expected {licenca:?}",
10129 );
10130 }
10131 }
10132
10133 // ── Caixa::repositorio — outer top-level Option<&str> scalar accessor ──
10134
10135 #[test]
10136 fn repositorio_returns_repositorio_byte_string_verbatim_across_permutations() {
10137 // The canonical per-`Caixa` `:repositorio` git-repo-URL scalar
10138 // pin: [`Caixa::repositorio`] must return the `:repositorio`
10139 // typed byte-string verbatim as an `Option<&str>`, byte-equal
10140 // to the raw `self.repositorio.as_deref()` access across every
10141 // representative value in the accept-set — `None` (the "omit
10142 // the slot to defer to the per-renderer placeholder" arm every
10143 // existing fixture without a `:repositorio` line carries),
10144 // `Some("")` (a past-the-guard sentinel that pins the accessor
10145 // doesn't perform a silent `Some("") → None` collapse on the
10146 // empty arm — validate rejects `Some("")` through
10147 // `RepositorioEmpty` but the accessor must ship the raw slot
10148 // verbatim so a validate-time gate regression surfaces at the
10149 // caixa-helm / caixa-flux emit boundary rather than being
10150 // silently absorbed into the per-renderer fallback),
10151 // `Some("github:pleme-io/hello-rio")` (the canonical `github:`
10152 // shorthand every existing manifest fixture across
10153 // `caixa-helm` / `caixa-mesh` and the `examples/` uses),
10154 // `Some("https://github.com/pleme-io/checkout")` (the canonical
10155 // `https://` URL the README quickstart uses),
10156 // `Some("ssh://git@github.com/pleme-io/checkout.git")` /
10157 // `Some("git://github.com/pleme-io/checkout.git")` /
10158 // `Some("git@github.com:pleme-io/checkout.git")` /
10159 // `Some("file:///opt/mirrors/pleme-io/checkout")` (every non-
10160 // github scheme the shared `is_git_repo_url` predicate
10161 // documents), and five past-the-guard sentinels for the
10162 // `RepositorioInvalid` refusal cases (`Some("pleme-io/checkout")`
10163 // missing-colon, `Some("-upload-pack=evil")` leading-dash, /
10164 // `Some("github:pleme-io/checkout?ref=main")` query-string, /
10165 // `Some("github:pleme-io/checkout#main")` fragment-anchor, /
10166 // `Some("github:pleme-io/{tpl}")` URI-template-placeholder — the
10167 // sentinels pin the accessor doesn't silently absorb the
10168 // refusal cases into a fallback).
10169 //
10170 // Second outer top-level [`Caixa`] `Option<&str>`-return scalar
10171 // accessor pin on the substrate primitive — sibling of the peer
10172 // [`Caixa::licenca`] (6d5bc28) pin
10173 // (`licenca_returns_licenca_byte_string_verbatim_across_permutations`)
10174 // that opened the "outer [`Caixa`] `Option<&str>` scalar"
10175 // projection pin pattern this pin folds on. Sibling in shape to
10176 // the peer per-`:placement`
10177 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
10178 // [`crate::aplicacao::Placement::affinity`] (74ec2d3) accessor
10179 // pins on the sibling per-M3-mesh-slot `Option<&str>`-return
10180 // axes, extended onto the outer top-level [`Caixa`] universal-
10181 // axis surface. Pins against a future silent detour that
10182 // returned an owned `Option<String>` (which would type-check
10183 // but silently allocate on every accessor call, breaking the
10184 // zero-cost projection every peer sibling accessor carries), a
10185 // `Some("") → None` collapse (which would silently absorb the
10186 // `RepositorioEmpty` refusal case at the accessor boundary and
10187 // the caixa-helm `Chart.yaml` `home:` fold would silently
10188 // render a `home: null` / omitted field on a struct-literal
10189 // `Caixa { repositorio: Some(""), .. }`), or a
10190 // `None → Some(<default>)` collapse (which would silently reify
10191 // the per-renderer fallback at the accessor boundary and every
10192 // downstream consumer keying off the `Option::is_none()`
10193 // discriminator would lose the "author omitted the slot"
10194 // signal).
10195 for repositorio in [
10196 None,
10197 Some(""),
10198 Some("github:pleme-io/hello-rio"),
10199 Some("https://github.com/pleme-io/checkout"),
10200 Some("ssh://git@github.com/pleme-io/checkout.git"),
10201 Some("git://github.com/pleme-io/checkout.git"),
10202 Some("git@github.com:pleme-io/checkout.git"),
10203 Some("file:///opt/mirrors/pleme-io/checkout"),
10204 Some("pleme-io/checkout"),
10205 Some("-upload-pack=evil"),
10206 Some("github:pleme-io/checkout?ref=main"),
10207 Some("github:pleme-io/checkout#main"),
10208 Some("github:pleme-io/{tpl}"),
10209 ] {
10210 let c = caixa_with_repositorio(repositorio);
10211 assert_eq!(
10212 c.repositorio(),
10213 repositorio,
10214 "Caixa::repositorio must return :repositorio verbatim \
10215 (got {:?}, expected {repositorio:?})",
10216 c.repositorio(),
10217 );
10218 assert_eq!(
10219 c.repositorio(),
10220 c.repositorio.as_deref(),
10221 "Caixa::repositorio must byte-equal the raw \
10222 `self.repositorio.as_deref()` field access across every \
10223 value in the Option<&str> accept-set",
10224 );
10225 }
10226 }
10227
10228 #[test]
10229 fn validate_repositorio_empty_arm_routes_through_accessor() {
10230 // Composition pin: [`Caixa::validate_repositorio`]'s empty-arm
10231 // gate must key off [`Caixa::repositorio`], not the raw
10232 // `self.repositorio.as_deref()` field access. Structurally: a
10233 // `Caixa { repositorio: Some(""), .. }` must surface the
10234 // `RepositorioEmpty` refusal exactly, and a
10235 // `Caixa { repositorio: Some("github:pleme-io/hello-rio"), .. }`
10236 // (the canonical `github:` shorthand form) must pass validate.
10237 // The pair jointly pins the accessor + validate-gate
10238 // composition: any future silent detour that had the accessor
10239 // return `None` on the empty arm (a `.filter(|s| !s.is_empty())`
10240 // collapse) would silently absorb the `RepositorioEmpty` refusal
10241 // at the accessor boundary and the validate gate would accept a
10242 // struct-literal `Caixa { repositorio: Some(""), .. }` — the
10243 // composition pin catches that at caixa-core build time.
10244 //
10245 // Peer of the [`Caixa::licenca`] (6d5bc28)
10246 // `validate_licenca_empty_arm_routes_through_accessor`
10247 // composition pin on the sibling outer top-level [`Caixa`]
10248 // `Option<&str>` universal-axis surface — same "the validate /
10249 // shape-gate predicate must route through the substrate-
10250 // primitive typed dispatch" discipline extended onto the second
10251 // outer top-level [`Caixa`] universal-axis `Option<&str>`-
10252 // composition surface.
10253 let c = caixa_with_repositorio(Some(""));
10254 assert!(
10255 matches!(
10256 c.validate_repositorio(),
10257 Err(ManifestError::RepositorioEmpty),
10258 ),
10259 "validate_repositorio must reject repositorio == Some(\"\") \
10260 with RepositorioEmpty — the accessor and the validate gate \
10261 must route through the same substrate-primitive typed \
10262 dispatch on the :repositorio empty arm",
10263 );
10264 let c = caixa_with_repositorio(Some("github:pleme-io/hello-rio"));
10265 assert!(
10266 c.validate_repositorio().is_ok(),
10267 "validate_repositorio must accept repositorio == \
10268 Some(\"github:pleme-io/hello-rio\") (the canonical \
10269 `github:` shorthand git-repo-URL shape)",
10270 );
10271 }
10272
10273 #[test]
10274 fn repositorio_projects_option_str_by_borrow() {
10275 // The by-borrow pin: [`Caixa::repositorio`] returns
10276 // `Option<&str>` by borrow — the `&str` borrows the underlying
10277 // `String` storage of the `Option<String>` slot and the
10278 // accessor must not allocate a fresh `String` on every call.
10279 // Peer of the per-`:placement`
10280 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) and the
10281 // [`Caixa::licenca`] (6d5bc28) by-borrow pins on the peer
10282 // `Option<&str>`-return axes, extended onto the second outer
10283 // top-level [`Caixa`] universal-axis `Option<&str>` shape —
10284 // the accessor's returned `&str` must borrow from `&self` (the
10285 // returned reference's lifetime is tied to `&self`), and
10286 // calling the accessor twice on the same [`Caixa`] must yield
10287 // the same `Option<&str>` verbatim (idempotent, no side effects
10288 // on `&self`).
10289 //
10290 // Pins against a future silent detour that returned an owned
10291 // `Option<String>` (which would type-check but silently
10292 // allocate on every call, breaking the zero-cost projection
10293 // every peer sibling accessor carries), or a one-arm-only
10294 // accessor that returned a saturating value on some sentinel
10295 // input (breaking the pass-through invariant the sibling
10296 // required-scalar accessors carry).
10297 for repositorio in [
10298 None,
10299 Some(""),
10300 Some("github:pleme-io/hello-rio"),
10301 Some("https://github.com/pleme-io/checkout"),
10302 ] {
10303 let c = caixa_with_repositorio(repositorio);
10304 let first = c.repositorio();
10305 let second = c.repositorio();
10306 assert_eq!(
10307 first, second,
10308 "Caixa::repositorio must be idempotent — two successive \
10309 calls on the same &self must return the same \
10310 Option<&str>",
10311 );
10312 assert_eq!(
10313 first, repositorio,
10314 "Caixa::repositorio must return :repositorio verbatim by \
10315 borrow — got {first:?}, expected {repositorio:?}",
10316 );
10317 }
10318 }
10319
10320 // ── Caixa::descricao — outer top-level Option<&str> scalar accessor ──
10321
10322 #[test]
10323 fn descricao_returns_descricao_byte_string_verbatim_across_permutations() {
10324 // The canonical per-`Caixa` `:descricao` free-form-prose scalar
10325 // pin: [`Caixa::descricao`] must return the `:descricao` typed
10326 // byte-string verbatim as an `Option<&str>`, byte-equal to the
10327 // raw `self.descricao.as_deref()` access across every
10328 // representative value in the accept-set — `None` (the "omit
10329 // the slot to defer to the per-renderer `caixa.nome`-derived
10330 // fallback" arm every existing fixture without a `:descricao`
10331 // line carries), `Some("")` (a past-the-guard sentinel that
10332 // pins the accessor doesn't perform a silent `Some("") → None`
10333 // collapse on the empty arm — validate rejects `Some("")`
10334 // through `DescricaoEmpty` but the accessor must ship the raw
10335 // slot verbatim so a validate-time gate regression surfaces at
10336 // the caixa-helm / caixa-feira emit boundary rather than being
10337 // silently absorbed into the per-renderer `caixa.nome`-derived
10338 // fallback), `Some("Checkout flow.")` (the canonical one-line
10339 // prose descriptor the peer
10340 // `validate_descricao_accepts_canonical_value` positive sweep
10341 // exercises), `Some("Canonical Rust→wasm32-wasip2 caixa
10342 // Servico.")` (the multi-byte Unicode continuation-byte shape
10343 // the `hello-rio` fixture carries), `Some("→ — · ✓")` (a
10344 // multi-glyph Unicode shape the peer
10345 // `is_chart_description_shape` predicate accepts), and five
10346 // past-the-guard sentinels for the `DescricaoInvalid` refusal
10347 // cases (`Some(" Checkout flow.")` leading-whitespace,
10348 // `Some("Checkout flow. ")` trailing-whitespace,
10349 // `Some("Checkout\nflow.")` embedded-LF,
10350 // `Some("Checkout\tflow.")` embedded-TAB, and
10351 // `Some("Checkout\x00flow.")` embedded-NUL — the sentinels pin
10352 // the accessor doesn't silently absorb the refusal cases into
10353 // a fallback).
10354 //
10355 // Third outer top-level [`Caixa`] `Option<&str>`-return scalar
10356 // accessor pin on the substrate primitive — sibling of the peer
10357 // [`Caixa::licenca`] (6d5bc28) and [`Caixa::repositorio`]
10358 // (cc7332d) pins that opened the "outer [`Caixa`]
10359 // `Option<&str>` scalar" projection pin pattern this pin folds
10360 // on. Sibling in shape to the peer per-`:placement`
10361 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
10362 // [`crate::aplicacao::Placement::affinity`] (74ec2d3) accessor
10363 // pins on the sibling per-M3-mesh-slot `Option<&str>`-return
10364 // axes, extended onto the outer top-level [`Caixa`] universal-
10365 // axis surface. Pins against a future silent detour that
10366 // returned an owned `Option<String>` (which would type-check
10367 // but silently allocate on every accessor call, breaking the
10368 // zero-cost projection every peer sibling accessor carries), a
10369 // `Some("") → None` collapse (which would silently absorb the
10370 // `DescricaoEmpty` refusal case at the accessor boundary and
10371 // the caixa-helm `Chart.yaml` `description:` fold would
10372 // silently render a `caixa.nome`-derived fallback on a
10373 // struct-literal `Caixa { descricao: Some(""), .. }`), or a
10374 // `None → Some(<default>)` collapse (which would silently
10375 // reify the per-renderer `caixa.nome`-derived fallback at the
10376 // accessor boundary and every downstream consumer keying off
10377 // the `Option::is_none()` discriminator would lose the "author
10378 // omitted the slot" signal).
10379 for descricao in [
10380 None,
10381 Some(""),
10382 Some("Checkout flow."),
10383 Some("Canonical Rust→wasm32-wasip2 caixa Servico."),
10384 Some("→ — · ✓"),
10385 Some(" Checkout flow."),
10386 Some("Checkout flow. "),
10387 Some("Checkout\nflow."),
10388 Some("Checkout\tflow."),
10389 Some("Checkout\x00flow."),
10390 ] {
10391 let c = caixa_with_descricao(descricao);
10392 assert_eq!(
10393 c.descricao(),
10394 descricao,
10395 "Caixa::descricao must return :descricao verbatim (got \
10396 {:?}, expected {descricao:?})",
10397 c.descricao(),
10398 );
10399 assert_eq!(
10400 c.descricao(),
10401 c.descricao.as_deref(),
10402 "Caixa::descricao must byte-equal the raw \
10403 `self.descricao.as_deref()` field access across every \
10404 value in the Option<&str> accept-set",
10405 );
10406 }
10407 }
10408
10409 #[test]
10410 fn validate_descricao_empty_arm_routes_through_accessor() {
10411 // Composition pin: [`Caixa::validate_descricao`]'s empty-arm
10412 // gate must key off [`Caixa::descricao`], not the raw
10413 // `self.descricao.as_deref()` field access. Structurally: a
10414 // `Caixa { descricao: Some(""), .. }` must surface the
10415 // `DescricaoEmpty` refusal exactly, and a
10416 // `Caixa { descricao: Some("Checkout flow."), .. }` (the
10417 // canonical one-line-prose form) must pass validate. The pair
10418 // jointly pins the accessor + validate-gate composition: any
10419 // future silent detour that had the accessor return `None` on
10420 // the empty arm (a `.filter(|s| !s.is_empty())` collapse) would
10421 // silently absorb the `DescricaoEmpty` refusal at the accessor
10422 // boundary and the validate gate would accept a struct-literal
10423 // `Caixa { descricao: Some(""), .. }` — the composition pin
10424 // catches that at caixa-core build time.
10425 //
10426 // Peer of the [`Caixa::licenca`] (6d5bc28)
10427 // `validate_licenca_empty_arm_routes_through_accessor` and
10428 // [`Caixa::repositorio`] (cc7332d)
10429 // `validate_repositorio_empty_arm_routes_through_accessor`
10430 // composition pins on the sibling outer top-level [`Caixa`]
10431 // `Option<&str>` universal-axis surface — same "the validate /
10432 // shape-gate predicate must route through the substrate-
10433 // primitive typed dispatch" discipline extended onto the third
10434 // outer top-level [`Caixa`] universal-axis `Option<&str>`-
10435 // composition surface.
10436 let c = caixa_with_descricao(Some(""));
10437 assert!(
10438 matches!(c.validate_descricao(), Err(ManifestError::DescricaoEmpty),),
10439 "validate_descricao must reject descricao == Some(\"\") \
10440 with DescricaoEmpty — the accessor and the validate gate \
10441 must route through the same substrate-primitive typed \
10442 dispatch on the :descricao empty arm",
10443 );
10444 let c = caixa_with_descricao(Some("Checkout flow."));
10445 assert!(
10446 c.validate_descricao().is_ok(),
10447 "validate_descricao must accept descricao == \
10448 Some(\"Checkout flow.\") (the canonical one-line-prose \
10449 chart-description shape)",
10450 );
10451 }
10452
10453 #[test]
10454 fn descricao_projects_option_str_by_borrow() {
10455 // The by-borrow pin: [`Caixa::descricao`] returns
10456 // `Option<&str>` by borrow — the `&str` borrows the underlying
10457 // `String` storage of the `Option<String>` slot and the
10458 // accessor must not allocate a fresh `String` on every call.
10459 // Peer of the [`Caixa::licenca`] (6d5bc28) and
10460 // [`Caixa::repositorio`] (cc7332d) by-borrow pins on the peer
10461 // outer top-level [`Caixa`] `Option<&str>`-return axes, and of
10462 // the per-`:placement`
10463 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) by-
10464 // borrow pin on the peer per-M3-mesh-slot `Option<&str>`-
10465 // return axis, extended onto the third outer top-level
10466 // [`Caixa`] universal-axis `Option<&str>` shape — the
10467 // accessor's returned `&str` must borrow from `&self` (the
10468 // returned reference's lifetime is tied to `&self`), and
10469 // calling the accessor twice on the same [`Caixa`] must yield
10470 // the same `Option<&str>` verbatim (idempotent, no side
10471 // effects on `&self`).
10472 //
10473 // Pins against a future silent detour that returned an owned
10474 // `Option<String>` (which would type-check but silently
10475 // allocate on every call, breaking the zero-cost projection
10476 // every peer sibling accessor carries), or a one-arm-only
10477 // accessor that returned a saturating value on some sentinel
10478 // input (breaking the pass-through invariant the sibling
10479 // required-scalar accessors carry).
10480 for descricao in [
10481 None,
10482 Some(""),
10483 Some("Checkout flow."),
10484 Some("Canonical Rust→wasm32-wasip2 caixa Servico."),
10485 ] {
10486 let c = caixa_with_descricao(descricao);
10487 let first = c.descricao();
10488 let second = c.descricao();
10489 assert_eq!(
10490 first, second,
10491 "Caixa::descricao must be idempotent — two successive \
10492 calls on the same &self must return the same \
10493 Option<&str>",
10494 );
10495 assert_eq!(
10496 first, descricao,
10497 "Caixa::descricao must return :descricao verbatim by \
10498 borrow — got {first:?}, expected {descricao:?}",
10499 );
10500 }
10501 }
10502
10503 // ── validate_edicao — universal-axis language-edition shape ──
10504
10505 fn caixa_with_edicao(edicao: Option<&str>) -> Caixa {
10506 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
10507 c.edicao = edicao.map(String::from);
10508 c
10509 }
10510
10511 #[test]
10512 fn validate_edicao_accepts_none() {
10513 // The omit-the-slot identity: `:edicao` is optional. The
10514 // gate is a no-op when the author didn't declare a value —
10515 // every caixa without an `:edicao` line trivially passes,
10516 // and the substrate-side build pipeline falls back to the
10517 // documented default edition. Mirrors the peer
10518 // `validate_licenca_accepts_none` posture on the sibling
10519 // `Option<String>` Caixa slot.
10520 let c = caixa_with_edicao(None);
10521 c.validate_edicao().unwrap();
10522 }
10523
10524 #[test]
10525 fn validate_edicao_accepts_canonical_value() {
10526 // Positive control: the canonical `"2026"` edition every
10527 // existing renderer-side fixture (`caixa-helm`, `caixa-flux`,
10528 // `caixa-mesh`) carries by construction passes the gate.
10529 // Future-introduced sibling editions (`"2027"`, `"2030"`,
10530 // `"2049"`) that match the same 4-digit ASCII decimal year
10531 // shape must also trivially pass — the structural shape
10532 // predicate accepts every well-formed year regardless of
10533 // whether the substrate yet understands the specific value
10534 // (a future known-edition allowlist tightens that).
10535 for ed in ["2026", "2027", "2030", "2049"] {
10536 let c = caixa_with_edicao(Some(ed));
10537 c.validate_edicao()
10538 .unwrap_or_else(|err| panic!("canonical {ed:?} must pass: {err:?}"));
10539 }
10540 }
10541
10542 #[test]
10543 fn validate_edicao_rejects_empty_some() {
10544 // Canonical paste-from-blank-doc footgun. Without this gate
10545 // the empty `Some("")` silently lands as `(:edicao "")` in
10546 // the rendered caixa.lisp and a future renderer-side
10547 // consumer's `Option::unwrap_or_else` (which only fires on
10548 // `None`) skips its fallback. Mirrors the peer
10549 // [`ManifestError::LicencaEmpty`] empty-arm on the sibling
10550 // `Option<String>` Caixa slot.
10551 let c = caixa_with_edicao(Some(""));
10552 let err = c.validate_edicao().unwrap_err();
10553 assert!(matches!(err, ManifestError::EdicaoEmpty), "got {err:?}",);
10554 }
10555
10556 #[test]
10557 fn validate_edicao_rejects_free_form_non_year() {
10558 // Free-form non-year footgun: the bare `"x"` / `"latest"` /
10559 // `"nightly"` shapes carry no operational meaning on the
10560 // substrate's build-time edition selector. Until this gate
10561 // landed the bare empty-arm check let every such value
10562 // through and broke far from the source caixa.lisp. Peer
10563 // with the shape-predicate cascade
10564 // `validate_repositorio_rejects_missing_colon_separator`
10565 // establishes past its own empty arm.
10566 for ed in ["x", "latest", "nightly", "stable"] {
10567 let c = caixa_with_edicao(Some(ed));
10568 let err = c.validate_edicao().unwrap_err();
10569 assert!(
10570 matches!(err, ManifestError::EdicaoInvalid { .. }),
10571 "expected EdicaoInvalid on {ed:?}, got {err:?}",
10572 );
10573 }
10574 }
10575
10576 #[test]
10577 fn validate_edicao_rejects_trailing_whitespace() {
10578 // Paste-from-doc whitespace footgun. A trailing space in
10579 // the `:edicao` value would silently break the substrate's
10580 // build-time edition match-table lookup at the rendered
10581 // artifact's edition-selector consumer. The shape predicate
10582 // refuses every whitespace byte by construction (any byte
10583 // outside `0-9` fails `is_ascii_digit`). Peer with
10584 // `validate_repositorio_rejects_whitespace`.
10585 let c = caixa_with_edicao(Some("2026 "));
10586 let err = c.validate_edicao().unwrap_err();
10587 let ManifestError::EdicaoInvalid { edicao, .. } = err else {
10588 panic!("expected EdicaoInvalid, got {err:?}");
10589 };
10590 assert_eq!(edicao, "2026 ");
10591 }
10592
10593 #[test]
10594 fn validate_edicao_rejects_leading_whitespace() {
10595 // Symmetric paste-from-doc whitespace footgun on the leading
10596 // boundary — the gate refuses every shape with a non-digit
10597 // byte by construction.
10598 let c = caixa_with_edicao(Some(" 2026"));
10599 let err = c.validate_edicao().unwrap_err();
10600 assert!(
10601 matches!(err, ManifestError::EdicaoInvalid { .. }),
10602 "got {err:?}",
10603 );
10604 }
10605
10606 #[test]
10607 fn validate_edicao_rejects_control_char() {
10608 // Paste-from-multiline-doc CRLF footgun — control characters
10609 // at the value boundary break the substrate's build-time
10610 // edition-selector parser. Peer with
10611 // `validate_repositorio_rejects_control_char`.
10612 let c = caixa_with_edicao(Some("2026\n"));
10613 let err = c.validate_edicao().unwrap_err();
10614 assert!(
10615 matches!(err, ManifestError::EdicaoInvalid { .. }),
10616 "got {err:?}",
10617 );
10618 }
10619
10620 #[test]
10621 fn validate_edicao_rejects_non_ascii_lookalike() {
10622 // Fullwidth-keyboard look-alike footgun — `"2026"` is
10623 // the U+FF12 U+FF10 U+FF12 U+FF16 sequence (CJK fullwidth
10624 // digits), 4 codepoints but 12 UTF-8 bytes; the substrate's
10625 // edition selector wants an ASCII year, and the gate
10626 // refuses every non-ASCII shape by construction (length in
10627 // bytes is 12 ≠ 4, *and* every byte falls outside
10628 // `is_ascii_digit`'s `0-9` range).
10629 let c = caixa_with_edicao(Some("2026"));
10630 let err = c.validate_edicao().unwrap_err();
10631 assert!(
10632 matches!(err, ManifestError::EdicaoInvalid { .. }),
10633 "got {err:?}",
10634 );
10635 }
10636
10637 #[test]
10638 fn validate_edicao_rejects_version_tag_prefix() {
10639 // Common version-tag idiom footgun — `"v2026"` / `"e2026"`
10640 // / `"r2026"` are familiar shapes from git-tag / Rust
10641 // edition / release-tag conventions that don't apply to
10642 // the year-shaped edition axis. The shape predicate refuses
10643 // every leading non-digit prefix.
10644 for ed in ["v2026", "e2026", "r2026"] {
10645 let c = caixa_with_edicao(Some(ed));
10646 let err = c.validate_edicao().unwrap_err();
10647 assert!(
10648 matches!(err, ManifestError::EdicaoInvalid { .. }),
10649 "expected EdicaoInvalid on {ed:?}, got {err:?}",
10650 );
10651 }
10652 }
10653
10654 #[test]
10655 fn validate_edicao_rejects_decimal_shape() {
10656 // Decimal-shaped pseudo-version footgun — `"2026.1"` /
10657 // `"2026.0"` are familiar shapes from semver / float
10658 // conventions that don't apply to the year-shaped edition
10659 // axis. The shape predicate refuses every non-digit byte
10660 // (`.` falls outside `is_ascii_digit`).
10661 for ed in ["2026.1", "2026.0", "2026.0.1"] {
10662 let c = caixa_with_edicao(Some(ed));
10663 let err = c.validate_edicao().unwrap_err();
10664 assert!(
10665 matches!(err, ManifestError::EdicaoInvalid { .. }),
10666 "expected EdicaoInvalid on {ed:?}, got {err:?}",
10667 );
10668 }
10669 }
10670
10671 #[test]
10672 fn validate_edicao_rejects_wrong_length_numeric() {
10673 // Wrong-length numeric footgun — `"26"` (truncated) /
10674 // `"202"` (truncated) / `"20260"` (extra digit) / `"00026"`
10675 // (zero-padded too wide) all parse as integers but don't
10676 // name a 4-digit year. The shape predicate refuses every
10677 // value whose length isn't exactly 4 bytes.
10678 for ed in ["26", "202", "20260", "00026", "9"] {
10679 let c = caixa_with_edicao(Some(ed));
10680 let err = c.validate_edicao().unwrap_err();
10681 assert!(
10682 matches!(err, ManifestError::EdicaoInvalid { .. }),
10683 "expected EdicaoInvalid on {ed:?}, got {err:?}",
10684 );
10685 }
10686 }
10687
10688 #[test]
10689 fn validate_edicao_empty_takes_precedence_over_shape() {
10690 // Empty-first cascade pin: the empty `Some("")` surfaces
10691 // the narrower `EdicaoEmpty` not the shape-predicate-
10692 // wrapped `EdicaoInvalid`, mirroring the peer
10693 // `validate_repositorio_empty_takes_precedence_over_shape`
10694 // (`RepositorioEmpty` → `RepositorioInvalid`),
10695 // `NomeEmpty` → `NomeInvalid`, `VersaoEmpty` →
10696 // `VersaoInvalid`, `FonteRepoEmpty` → `FonteRepoInvalid`
10697 // cascades. The shape predicate also refuses the empty
10698 // input (defensively — `s.len() != 4`), but the
10699 // manifest-layer empty arm runs first to surface the
10700 // narrower diagnostic verbatim.
10701 let c = caixa_with_edicao(Some(""));
10702 let err = c.validate_edicao().unwrap_err();
10703 assert!(matches!(err, ManifestError::EdicaoEmpty), "got {err:?}",);
10704 }
10705
10706 #[test]
10707 fn validate_edicao_template_passes() {
10708 // Round-trip pin: the bare `Caixa::template` shape (which
10709 // carries `:edicao "2026"` verbatim) passes the gate by
10710 // construction. A future template-shape change that
10711 // introduced `(:edicao "")` or a non-year value would
10712 // surface here as a regression. Mirrors the peer
10713 // `validate_licenca_template_passes` pin.
10714 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
10715 c.validate_edicao().unwrap();
10716 }
10717
10718 #[test]
10719 fn validate_edicao_diagnostic_names_offending_slot() {
10720 // Diagnostic-shape pin (peer with
10721 // `validate_licenca_diagnostic_names_offending_slot`): the
10722 // error's Display surfaces the `:edicao` slot name verbatim,
10723 // so a `feira lint` run can render the diagnostic without
10724 // re-parsing and the author can grep their caixa.lisp for
10725 // the offending `:edicao` line.
10726 let c = caixa_with_edicao(Some(""));
10727 let rendered = c.validate_edicao().unwrap_err().to_string();
10728 assert!(
10729 rendered.contains(":edicao"),
10730 "diagnostic must name the offending slot: {rendered}",
10731 );
10732 }
10733
10734 #[test]
10735 fn validate_edicao_invalid_diagnostic_carries_offending_value() {
10736 // Diagnostic-shape pin on the shape-predicate arm (peer
10737 // with `validate_repositorio_diagnostic_carries_offending_value`):
10738 // the error's Display surfaces the offending value + slot
10739 // name verbatim, so a `feira lint` run can render the
10740 // diagnostic without re-parsing and the author can grep
10741 // their caixa.lisp for the offending `:edicao` value.
10742 let c = caixa_with_edicao(Some("v2026"));
10743 let rendered = c.validate_edicao().unwrap_err().to_string();
10744 assert!(
10745 rendered.contains(":edicao"),
10746 "diagnostic must name the offending slot: {rendered}",
10747 );
10748 assert!(
10749 rendered.contains("v2026"),
10750 "diagnostic must quote the offending value: {rendered}",
10751 );
10752 }
10753
10754 // ── Caixa::edicao — outer top-level Option<&str> scalar accessor ──
10755
10756 #[test]
10757 fn edicao_returns_edicao_byte_string_verbatim_across_permutations() {
10758 // The canonical per-`Caixa` `:edicao` language-edition scalar
10759 // pin: [`Caixa::edicao`] must return the `:edicao` typed
10760 // byte-string verbatim as an `Option<&str>`, byte-equal to the
10761 // raw `self.edicao.as_deref()` access across every representative
10762 // value in the accept-set — `None` (the "omit the slot to defer
10763 // to the substrate's default edition" arm every existing
10764 // [`caixa-resolver`] fixture without an `:edicao` line carries),
10765 // `Some("")` (a past-the-guard sentinel that pins the accessor
10766 // doesn't perform a silent `Some("") → None` collapse on the
10767 // empty arm — validate rejects `Some("")` through `EdicaoEmpty`
10768 // but the accessor must ship the raw slot verbatim so a
10769 // validate-time gate regression surfaces at any future edition-
10770 // aware consumer's boundary rather than being silently absorbed
10771 // into the substrate's default edition), `Some("2026")` (the
10772 // canonical 4-digit-ASCII-decimal-year shape every `feira init`
10773 // template scaffolds via [`Caixa::template`] and every
10774 // renderer-side fixture at `caixa-helm/src/lib.rs:978` /
10775 // `caixa-flux/src/lib.rs:2319` / `caixa-mesh/src/lib.rs:3208`
10776 // carries by construction), `Some("2018")` / `Some("2021")` /
10777 // `Some("2024")` (canonical 4-digit-ASCII-decimal-year shapes
10778 // peer with Cargo's `[package] edition` grammar every future-
10779 // introduced sibling to `"2026"` will follow), and eight
10780 // past-the-guard sentinels for the `EdicaoInvalid` refusal cases
10781 // (`Some("2026 ")` trailing-whitespace, `Some(" 2026")` leading-
10782 // whitespace, `Some("2026\n")` embedded-LF, `Some("2026")`
10783 // fullwidth-non-ASCII-lookalike, `Some("v2026")` version-tag-
10784 // prefix, `Some("2026.1")` decimal-shape, `Some("26")` wrong-
10785 // length-numeric, `Some("latest")` free-form-non-year — the
10786 // sentinels pin the accessor doesn't silently absorb the
10787 // refusal cases into a substrate-default-edition fallback).
10788 //
10789 // Fourth and final outer top-level [`Caixa`] `Option<&str>`-
10790 // return scalar accessor pin on the substrate primitive —
10791 // sibling of the peer [`Caixa::licenca`] (6d5bc28),
10792 // [`Caixa::repositorio`] (cc7332d), and [`Caixa::descricao`]
10793 // (3f16e2f) pins that opened the "outer [`Caixa`]
10794 // `Option<&str>` scalar" projection pin pattern this pin folds
10795 // on. Sibling in shape to the peer per-`:placement`
10796 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
10797 // [`crate::aplicacao::Placement::affinity`] (74ec2d3) accessor
10798 // pins on the sibling per-M3-mesh-slot `Option<&str>`-return
10799 // axes, extended onto the outer top-level [`Caixa`] universal-
10800 // axis surface's last unlifted `Option<String>` slot. Pins
10801 // against a future silent detour that returned an owned
10802 // `Option<String>` (which would type-check but silently
10803 // allocate on every accessor call, breaking the zero-cost
10804 // projection every peer sibling accessor carries), a
10805 // `Some("") → None` collapse (which would silently absorb the
10806 // `EdicaoEmpty` refusal case at the accessor boundary and any
10807 // future edition-aware consumer would silently fall back to
10808 // the substrate's default edition on a struct-literal
10809 // `Caixa { edicao: Some(""), .. }`), or a
10810 // `None → Some("2026")` collapse (which would silently reify
10811 // the substrate's default edition at the accessor boundary
10812 // and every downstream consumer keying off the
10813 // `Option::is_none()` discriminator would lose the "author
10814 // omitted the slot" signal).
10815 for edicao in [
10816 None,
10817 Some(""),
10818 Some("2026"),
10819 Some("2018"),
10820 Some("2021"),
10821 Some("2024"),
10822 Some("2026 "),
10823 Some(" 2026"),
10824 Some("2026\n"),
10825 Some("2026"),
10826 Some("v2026"),
10827 Some("2026.1"),
10828 Some("26"),
10829 Some("latest"),
10830 ] {
10831 let c = caixa_with_edicao(edicao);
10832 assert_eq!(
10833 c.edicao(),
10834 edicao,
10835 "Caixa::edicao must return :edicao verbatim (got {:?}, \
10836 expected {edicao:?})",
10837 c.edicao(),
10838 );
10839 assert_eq!(
10840 c.edicao(),
10841 c.edicao.as_deref(),
10842 "Caixa::edicao must byte-equal the raw \
10843 `self.edicao.as_deref()` field access across every \
10844 value in the Option<&str> accept-set",
10845 );
10846 }
10847 }
10848
10849 #[test]
10850 fn validate_edicao_empty_arm_routes_through_accessor() {
10851 // Composition pin: [`Caixa::validate_edicao`]'s empty-arm gate
10852 // must key off [`Caixa::edicao`], not the raw
10853 // `self.edicao.as_deref()` field access. Structurally: a
10854 // `Caixa { edicao: Some(""), .. }` must surface the
10855 // `EdicaoEmpty` refusal exactly, and a
10856 // `Caixa { edicao: Some("2026"), .. }` (the canonical
10857 // 4-digit-ASCII-decimal-year form) must pass validate. The
10858 // pair jointly pins the accessor + validate-gate composition:
10859 // any future silent detour that had the accessor return `None`
10860 // on the empty arm (a `.filter(|s| !s.is_empty())` collapse)
10861 // would silently absorb the `EdicaoEmpty` refusal at the
10862 // accessor boundary and the validate gate would accept a
10863 // struct-literal `Caixa { edicao: Some(""), .. }` — the
10864 // composition pin catches that at caixa-core build time.
10865 //
10866 // Peer of the [`Caixa::licenca`] (6d5bc28)
10867 // `validate_licenca_empty_arm_routes_through_accessor`,
10868 // [`Caixa::repositorio`] (cc7332d)
10869 // `validate_repositorio_empty_arm_routes_through_accessor`,
10870 // and [`Caixa::descricao`] (3f16e2f)
10871 // `validate_descricao_empty_arm_routes_through_accessor`
10872 // composition pins on the sibling outer top-level [`Caixa`]
10873 // `Option<&str>` universal-axis surface — same "the validate /
10874 // shape-gate predicate must route through the substrate-
10875 // primitive typed dispatch" discipline extended onto the
10876 // fourth and final outer top-level [`Caixa`] universal-axis
10877 // `Option<&str>`-composition surface, closing the accessor-
10878 // composition family.
10879 let c = caixa_with_edicao(Some(""));
10880 assert!(
10881 matches!(c.validate_edicao(), Err(ManifestError::EdicaoEmpty)),
10882 "validate_edicao must reject edicao == Some(\"\") with \
10883 EdicaoEmpty — the accessor and the validate gate must \
10884 route through the same substrate-primitive typed dispatch \
10885 on the :edicao empty arm",
10886 );
10887 let c = caixa_with_edicao(Some("2026"));
10888 assert!(
10889 c.validate_edicao().is_ok(),
10890 "validate_edicao must accept edicao == Some(\"2026\") \
10891 (the canonical 4-digit-ASCII-decimal-year shape)",
10892 );
10893 }
10894
10895 #[test]
10896 fn edicao_projects_option_str_by_borrow() {
10897 // The by-borrow pin: [`Caixa::edicao`] returns
10898 // `Option<&str>` by borrow — the `&str` borrows the underlying
10899 // `String` storage of the `Option<String>` slot and the
10900 // accessor must not allocate a fresh `String` on every call.
10901 // Peer of the [`Caixa::licenca`] (6d5bc28),
10902 // [`Caixa::repositorio`] (cc7332d), and [`Caixa::descricao`]
10903 // (3f16e2f) by-borrow pins on the peer outer top-level
10904 // [`Caixa`] `Option<&str>`-return axes, and of the
10905 // per-`:placement`
10906 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) by-
10907 // borrow pin on the peer per-M3-mesh-slot `Option<&str>`-
10908 // return axis, extended onto the fourth and final outer top-
10909 // level [`Caixa`] universal-axis `Option<&str>` shape — the
10910 // accessor's returned `&str` must borrow from `&self` (the
10911 // returned reference's lifetime is tied to `&self`), and
10912 // calling the accessor twice on the same [`Caixa`] must yield
10913 // the same `Option<&str>` verbatim (idempotent, no side
10914 // effects on `&self`).
10915 //
10916 // Pins against a future silent detour that returned an owned
10917 // `Option<String>` (which would type-check but silently
10918 // allocate on every call, breaking the zero-cost projection
10919 // every peer sibling accessor carries), or a one-arm-only
10920 // accessor that returned a saturating value on some sentinel
10921 // input (breaking the pass-through invariant the sibling
10922 // required-scalar accessors carry).
10923 for edicao in [None, Some(""), Some("2026"), Some("2018")] {
10924 let c = caixa_with_edicao(edicao);
10925 let first = c.edicao();
10926 let second = c.edicao();
10927 assert_eq!(
10928 first, second,
10929 "Caixa::edicao must be idempotent — two successive \
10930 calls on the same &self must return the same \
10931 Option<&str>",
10932 );
10933 assert_eq!(
10934 first, edicao,
10935 "Caixa::edicao must return :edicao verbatim by \
10936 borrow — got {first:?}, expected {edicao:?}",
10937 );
10938 }
10939 }
10940
10941 #[test]
10942 fn nome_returns_nome_byte_string_verbatim_across_permutations() {
10943 // The canonical per-`Caixa` `:nome` universal-axis DNS-1123-
10944 // label caixa-identity scalar pin: [`Caixa::nome`] must return
10945 // the `:nome` typed `String` verbatim as `&str`, byte-equal to
10946 // the raw field access across every representative value in
10947 // the accept-set — the canonical `"demo"` template baseline
10948 // (the same `feira init`-scaffolded default the sibling
10949 // `validate_nome_accepts_canonical_template` positive-control
10950 // gate pins), plus every sibling per-typed-slot atom accessor's
10951 // canonical positive-arm byte-string (`"catalog"` per
10952 // [`crate::aplicacao::Membro::nome`], `"cart"` per the peer
10953 // per-`:contratos` `:de`, `"hello-rio"` per the canonical
10954 // `caixa-helm`/`caixa-flux` cross-crate integration-test
10955 // fixture, `"checkout"` per the M3 mesh-slot Aplicacao
10956 // canonical example), plus every past-the-guard sentinel for
10957 // the `NomeEmpty` / `NomeInvalid` / `NomeChartNameBudgetExceeded`
10958 // refusal cases (`""`, `"Bad_Name"`, `"a"` × 56 — 56 bytes fits
10959 // the bare DNS-1123 63-byte cap but overflows the joint
10960 // `lareira-<nome>` chart-name budget the sibling
10961 // [`Caixa::validate_nome_chart_name_budget`] gate closes on).
10962 //
10963 // The past-the-guard sentinels pin the accessor doesn't
10964 // silently absorb the refusal cases into a template-derived
10965 // fallback (a future `.nome().is_empty().then(|| "demo")`
10966 // collapse would silently absorb the `NomeEmpty` refusal at
10967 // the accessor boundary and the validate gate would accept a
10968 // struct-literal `Caixa { nome: "".into(), .. }` — the pin
10969 // catches that at caixa-core build time).
10970 //
10971 // First outer top-level [`Caixa`] `&str`-return required-
10972 // scalar accessor pin — opens the "outer [`Caixa`] `&str`
10973 // required-scalar" projection pattern the sibling per-`Caixa`
10974 // `:versao` future lift folds on. Sibling in shape to the peer
10975 // per-`:membros` [`crate::aplicacao::Membro::nome`] (4a32abf)
10976 // required-`String`-carry accessor pin on the sibling per-
10977 // sub-struct required-axis, extended onto the outer top-level
10978 // [`Caixa`] universal-axis required-`String`-carry axis.
10979 for nome in [
10980 "demo",
10981 "catalog",
10982 "cart",
10983 "hello-rio",
10984 "checkout",
10985 "",
10986 "Bad_Name",
10987 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
10988 ] {
10989 let c = caixa_with_nome(nome);
10990 assert_eq!(
10991 c.nome(),
10992 nome,
10993 "Caixa::nome must return :nome verbatim (got {}, \
10994 expected {nome})",
10995 c.nome(),
10996 );
10997 assert_eq!(
10998 c.nome(),
10999 c.nome.as_str(),
11000 "Caixa::nome must byte-equal the raw .nome field \
11001 access across every value in the String accept-set",
11002 );
11003 }
11004 }
11005
11006 #[test]
11007 fn validate_nome_empty_arm_routes_through_accessor() {
11008 // Composition pin: [`Caixa::validate_nome`]'s empty-arm must
11009 // key off [`Caixa::nome`], not the raw `.nome` field access.
11010 // Structurally: a `Caixa { nome: "".into(), .. }` must surface
11011 // the `NomeEmpty` refusal exactly, and the canonical `"demo"`
11012 // template baseline (the peer positive-arm the sibling
11013 // `validate_nome_accepts_canonical_template` gate carves out)
11014 // must pass validate. The pair jointly pins the accessor +
11015 // validate-gate composition: any future silent detour that
11016 // had the accessor return a fresh `"demo"` on the empty arm
11017 // (a `.nome().is_empty().then(|| "demo")` fallback collapse)
11018 // would silently absorb the `NomeEmpty` refusal at the
11019 // accessor boundary and the validate gate would accept a
11020 // struct-literal `Caixa { nome: "".into(), .. }` — the
11021 // composition pin catches that at caixa-core build time.
11022 //
11023 // Peer of the sibling per-`Caixa`
11024 // `validate_licenca_empty_arm_routes_through_accessor` (6d5bc28)
11025 // / `validate_repositorio_empty_arm_routes_through_accessor`
11026 // (cc7332d) / `validate_descricao_empty_arm_routes_through_accessor`
11027 // (3f16e2f) / `validate_edicao_empty_arm_routes_through_accessor`
11028 // (2641cbd) composition pins on the sibling outer top-level
11029 // [`Caixa`] `Option<&str>` axes — same "the validate /
11030 // shape-gate predicate must route through the substrate-
11031 // primitive typed dispatch" discipline extended onto the peer
11032 // outer top-level [`Caixa`] required-`&str` composition axis.
11033 let c = caixa_with_nome("");
11034 assert!(
11035 matches!(c.validate_nome(), Err(ManifestError::NomeEmpty)),
11036 "validate_nome must reject nome == \"\" with NomeEmpty — \
11037 the accessor and the validate gate must route through the \
11038 same substrate-primitive typed dispatch on the :nome \
11039 empty-arm",
11040 );
11041 let c = caixa_with_nome("demo");
11042 assert!(
11043 c.validate_nome().is_ok(),
11044 "validate_nome must accept nome == \"demo\" (the canonical \
11045 DNS-1123-label template baseline)",
11046 );
11047 }
11048
11049 #[test]
11050 fn nome_projects_str_by_borrow() {
11051 // The by-borrow pin: [`Caixa::nome`] returns `&str` by borrow
11052 // — the `&str` borrows the underlying `String` storage of the
11053 // required `nome` slot and the accessor must not allocate a
11054 // fresh `String` on every call. Peer of the [`Caixa::licenca`]
11055 // (6d5bc28) / [`Caixa::repositorio`] (cc7332d) /
11056 // [`Caixa::descricao`] (3f16e2f) / [`Caixa::edicao`] (2641cbd)
11057 // by-borrow pins on the peer outer top-level [`Caixa`]
11058 // `Option<&str>`-return axes, extended onto the first outer
11059 // top-level [`Caixa`] required-`&str`-return axis — the
11060 // accessor's returned `&str` must borrow from `&self` (the
11061 // returned reference's lifetime is tied to `&self`), and
11062 // calling the accessor twice on the same [`Caixa`] must yield
11063 // the same `&str` verbatim (idempotent, no side effects on
11064 // `&self`).
11065 //
11066 // Pins against a future silent detour that returned an owned
11067 // `String` (which would type-check but silently allocate on
11068 // every call, breaking the zero-cost projection every peer
11069 // sibling accessor carries), an accidental
11070 // `.nome.to_lowercase()` detour that returned a fresh
11071 // allocation through an already-DNS-1123-lowercase-only
11072 // string (breaking a future `const fn` regression), or a
11073 // one-arm-only accessor that returned a canonicalized value
11074 // on some sentinel input (breaking the pass-through invariant
11075 // the sibling required-scalar accessors carry).
11076 for nome in ["demo", "catalog", "hello-rio", "checkout"] {
11077 let c = caixa_with_nome(nome);
11078 let first = c.nome();
11079 let second = c.nome();
11080 assert_eq!(
11081 first, second,
11082 "Caixa::nome must be idempotent — two successive calls \
11083 on the same &self must return the same &str",
11084 );
11085 assert_eq!(
11086 first, nome,
11087 "Caixa::nome must return :nome verbatim by borrow — \
11088 got {first}, expected {nome}",
11089 );
11090 }
11091 }
11092
11093 #[test]
11094 fn versao_returns_versao_byte_string_verbatim_across_permutations() {
11095 // The canonical per-`Caixa` `:versao` universal-axis SemVer-2
11096 // pinned-version scalar pin: [`Caixa::versao`] must return the
11097 // `:versao` typed `String` verbatim as `&str`, byte-equal to the
11098 // raw `.versao` field access across every representative value
11099 // in the accept-set — the canonical `"0.1.0"` template baseline
11100 // (the same `feira init`-scaffolded default the sibling
11101 // `validate_versao_accepts_canonical_template` positive-control
11102 // gate pins), plus every canonical SemVer-2 shape the sibling
11103 // `validate_versao_accepts_canonical_forms` positive-arm sweep
11104 // covers (`"0.0.0"`, `"1.0.0"`, `"0.2.0-rc.1"`,
11105 // `"1.0.0-alpha.0"`, `"1.0.0+build.42"`, `"1.0.0-rc.1+build.42"`,
11106 // `"10.20.30"`), plus every past-the-guard sentinel for the
11107 // `VersaoEmpty` / `VersaoInvalid` refusal cases (`""` the empty
11108 // arm, `"v0.1.0"` the git-tag-shape-leak footgun, `"0.1"` the
11109 // missing-patch footgun, `"^0.1"` the requirement-shape-leak
11110 // footgun, `"0.1.0.0"` the four-part-Java-convention footgun,
11111 // `"latest"` the docker-tag-shape footgun — the sentinels pin
11112 // the accessor doesn't silently absorb the refusal cases into a
11113 // template-derived fallback like `"0.1.0"`).
11114 //
11115 // The past-the-guard sentinels pin the accessor doesn't silently
11116 // absorb the refusal cases into a template-derived fallback (a
11117 // future `.versao().is_empty().then(|| "0.1.0")` collapse would
11118 // silently absorb the `VersaoEmpty` refusal at the accessor
11119 // boundary and the validate gate would accept a struct-literal
11120 // `Caixa { versao: "".into(), .. }` — the pin catches that at
11121 // caixa-core build time).
11122 //
11123 // Second outer top-level [`Caixa`] `&str`-return required-scalar
11124 // accessor pin — folds on the "outer [`Caixa`] `&str` required-
11125 // scalar" projection pattern the sibling per-`Caixa`
11126 // [`Caixa::nome`] (e6b7d97) opened. Sibling in shape to the peer
11127 // per-`:membros` [`crate::aplicacao::Membro::versao_requirement`]
11128 // (4127bb6) / per-`:children`
11129 // [`crate::supervisor::ChildSpec::versao_requirement`] (2c053c8)
11130 // / per-`:upgrade-from`
11131 // [`crate::UpgradeFromEntry::prior_versao`] (75d27a8) per-sub-
11132 // struct `:versao`-shaped `&str`-return accessor pins on the
11133 // sibling per-typed-slot version-carrier axes, extended onto the
11134 // second outer top-level [`Caixa`] universal-axis required-
11135 // `String`-carry axis so the two universal-axis identity-
11136 // carrying scalars every `defcaixa` form supplies (`:nome` +
11137 // `:versao`) share the same "one typed dispatch per axis" pin
11138 // discipline.
11139 for versao in [
11140 "0.1.0",
11141 "0.0.0",
11142 "1.0.0",
11143 "0.2.0-rc.1",
11144 "1.0.0-alpha.0",
11145 "1.0.0+build.42",
11146 "1.0.0-rc.1+build.42",
11147 "10.20.30",
11148 "",
11149 "v0.1.0",
11150 "0.1",
11151 "^0.1",
11152 "0.1.0.0",
11153 "latest",
11154 ] {
11155 let c = caixa_with_versao(versao);
11156 assert_eq!(
11157 c.versao(),
11158 versao,
11159 "Caixa::versao must return :versao verbatim (got {}, \
11160 expected {versao})",
11161 c.versao(),
11162 );
11163 assert_eq!(
11164 c.versao(),
11165 c.versao.as_str(),
11166 "Caixa::versao must byte-equal the raw .versao field \
11167 access across every value in the String accept-set",
11168 );
11169 }
11170 }
11171
11172 #[test]
11173 fn validate_versao_empty_arm_routes_through_accessor() {
11174 // Composition pin: [`Caixa::validate_versao`]'s empty-arm gate
11175 // must key off [`Caixa::versao`], not the raw `.versao` field
11176 // access. Structurally: a `Caixa { versao: "".into(), .. }` must
11177 // surface the `VersaoEmpty` refusal exactly, and the canonical
11178 // `"0.1.0"` template baseline (the peer positive-arm the sibling
11179 // `validate_versao_accepts_canonical_template` gate carves out)
11180 // must pass validate. The pair jointly pins the accessor +
11181 // validate-gate composition: any future silent detour that had
11182 // the accessor return a fresh `"0.1.0"` on the empty arm
11183 // (a `.versao().is_empty().then(|| "0.1.0")` fallback collapse)
11184 // would silently absorb the `VersaoEmpty` refusal at the
11185 // accessor boundary and the validate gate would accept a
11186 // struct-literal `Caixa { versao: "".into(), .. }` — the
11187 // composition pin catches that at caixa-core build time.
11188 //
11189 // Peer of the sibling per-`Caixa`
11190 // `validate_nome_empty_arm_routes_through_accessor` (e6b7d97)
11191 // composition pin on the sibling outer top-level [`Caixa`]
11192 // required-`&str` universal-axis surface — same "the validate /
11193 // shape-gate predicate must route through the substrate-
11194 // primitive typed dispatch" discipline extended onto the peer
11195 // outer top-level [`Caixa`] required-`&str` universal-axis
11196 // pinned-version composition axis, closing the second
11197 // coordinate of the "one canonical typed dispatch per per-Caixa
11198 // required-`&str` universal-axis" discipline.
11199 let c = caixa_with_versao("");
11200 assert!(
11201 matches!(c.validate_versao(), Err(ManifestError::VersaoEmpty)),
11202 "validate_versao must reject versao == \"\" with VersaoEmpty — \
11203 the accessor and the validate gate must route through the \
11204 same substrate-primitive typed dispatch on the :versao \
11205 empty-arm",
11206 );
11207 let c = caixa_with_versao("0.1.0");
11208 assert!(
11209 c.validate_versao().is_ok(),
11210 "validate_versao must accept versao == \"0.1.0\" (the \
11211 canonical SemVer-2 template baseline)",
11212 );
11213 }
11214
11215 #[test]
11216 fn versao_projects_str_by_borrow() {
11217 // The by-borrow pin: [`Caixa::versao`] returns `&str` by borrow
11218 // — the `&str` borrows the underlying `String` storage of the
11219 // required `versao` slot and the accessor must not allocate a
11220 // fresh `String` on every call. Peer of the [`Caixa::nome`]
11221 // (e6b7d97) by-borrow pin on the sibling outer top-level
11222 // [`Caixa`] required-`&str`-return axis, extended onto the
11223 // second outer top-level [`Caixa`] required-`&str`-return
11224 // universal-axis pinned-version surface — the accessor's
11225 // returned `&str` must borrow from `&self` (the returned
11226 // reference's lifetime is tied to `&self`), and calling the
11227 // accessor twice on the same [`Caixa`] must yield the same
11228 // `&str` verbatim (idempotent, no side effects on `&self`).
11229 //
11230 // Pins against a future silent detour that returned an owned
11231 // `String` (which would type-check but silently allocate on
11232 // every call, breaking the zero-cost projection every peer
11233 // sibling accessor carries), an accidental
11234 // `semver::Version::parse(&self.versao).unwrap().to_string()`
11235 // detour that returned a canonicalized fresh allocation through
11236 // an already-canonical byte-string (breaking a future `const fn`
11237 // regression and silently absorbing the `VersaoInvalid` refusal
11238 // at the accessor boundary), or a one-arm-only accessor that
11239 // returned a canonicalized value on some sentinel input
11240 // (breaking the pass-through invariant the sibling required-
11241 // scalar accessors carry).
11242 for versao in ["0.1.0", "1.0.0", "0.2.0-rc.1", "1.0.0+build.42"] {
11243 let c = caixa_with_versao(versao);
11244 let first = c.versao();
11245 let second = c.versao();
11246 assert_eq!(
11247 first, second,
11248 "Caixa::versao must be idempotent — two successive \
11249 calls on the same &self must return the same &str",
11250 );
11251 assert_eq!(
11252 first, versao,
11253 "Caixa::versao must return :versao verbatim by borrow \
11254 — got {first}, expected {versao}",
11255 );
11256 }
11257 }
11258
11259 fn caixa_with_kind(kind: CaixaKind) -> Caixa {
11260 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
11261 c.kind = kind;
11262 c
11263 }
11264
11265 #[test]
11266 fn kind_returns_kind_variant_verbatim_across_permutations() {
11267 // The canonical per-`Caixa` `:kind` universal-axis closed-set-
11268 // enum discriminant pin: [`Caixa::kind`] must return the `:kind`
11269 // typed [`CaixaKind`] variant verbatim by `Copy`, byte-equal to
11270 // the raw `.kind` field access across every variant in the
11271 // closed accept-set (`Biblioteca` — the library kind that
11272 // exports lisp forms; `Binario` — the nix-built executable kind
11273 // under `exe/`; `Servico` — the wasm-component daemon kind
11274 // under `servicos/`; `Supervisor` — the OTP-shaped hierarchical
11275 // reconciliation kind; `Aplicacao` — the M3 typed-mesh
11276 // composition kind).
11277 //
11278 // Pins against a future silent detour that re-derived the kind
11279 // from a peer axis (an accidental fallback to
11280 // `if !servicos.is_empty() { Servico } else if
11281 // !membros.is_empty() { Aplicacao } else { Biblioteca }`
11282 // collapse that read the code-surface / mesh-slot columns into
11283 // the kind discriminator), a variant remap the operator
11284 // authors on one consumer without the other, or a stale-derive
11285 // detour that substituted [`CaixaKind::Biblioteca`] as the
11286 // default when the field held any other variant (which would
11287 // silently collapse the distinction between "author explicitly
11288 // declared `:kind Servico`" and "author declared any other
11289 // kind" every downstream renderer-dispatch site depends on).
11290 //
11291 // First outer top-level [`Caixa`] `Copy`-return required-enum-
11292 // discriminant accessor pin — opens the "outer [`Caixa`]
11293 // `Copy`-return required-discriminant" projection pattern.
11294 // Sibling in shape to the peer per-`:supervisor`
11295 // [`crate::supervisor::SupervisorSpec::estrategia`] (eafb619),
11296 // per-`:placement` [`crate::aplicacao::Placement::estrategia`]
11297 // (921fe1b), and per-`:children`
11298 // [`crate::supervisor::ChildSpec::restart`] (dfb4a81)
11299 // `Copy`-return closed-set-enum discriminant accessor pins on
11300 // the sibling nested-spec typed-slot discriminator axes,
11301 // extended here to the outer top-level [`Caixa`] universal-
11302 // axis surface.
11303 for kind in [
11304 CaixaKind::Biblioteca,
11305 CaixaKind::Binario,
11306 CaixaKind::Servico,
11307 CaixaKind::Supervisor,
11308 CaixaKind::Aplicacao,
11309 ] {
11310 let c = caixa_with_kind(kind);
11311 assert_eq!(
11312 c.kind(),
11313 kind,
11314 "Caixa::kind must return :kind verbatim (got {:?}, \
11315 expected {kind:?})",
11316 c.kind(),
11317 );
11318 assert_eq!(
11319 c.kind(),
11320 c.kind,
11321 "Caixa::kind accessor and .kind field access must \
11322 byte-equal — the accessor is the substrate-primitive \
11323 typed dispatch every downstream kind-gate consumer \
11324 must route through",
11325 );
11326 }
11327 }
11328
11329 #[test]
11330 fn require_kind_reads_through_lifted_kind_accessor() {
11331 // Two-consumer coherence pin: the [`crate::render::require_kind`]
11332 // entry-gate predicate (the canonical two-line
11333 // `require_kind(caixa, Servico)?` prelude every per-Servico /
11334 // per-Aplicacao renderer runs at its entry-point) and the
11335 // sibling [`crate::render::KindMismatch`] error carrier's
11336 // `actual:` field (which names the offending caixa's variant
11337 // in the diagnostic) must both key off the lifted accessor, so
11338 // any future rebrand on the typed slot's reader shape lands at
11339 // exactly one place. Pins the two-site coherence by exercising
11340 // every off-diagonal `(actual, expected)` pair across the
11341 // closed accept-set — the `KindMismatch { actual, expected }`
11342 // surfaced on the mismatch arm must byte-equal the pair the
11343 // accessor returns for each side.
11344 //
11345 // Peer of the sibling per-`:placement`
11346 // `validate_placement_reads_through_lifted_estrategia_accessor`
11347 // (921fe1b) two-arm consumer-coherence pin on the M3 mesh-slot
11348 // `Copy`-return discriminant axis — same "the entry-gate
11349 // predicate and the error carrier's `actual:` field must route
11350 // through the substrate-primitive typed dispatch" discipline
11351 // extended onto the outer top-level [`Caixa`] universal-axis
11352 // discriminant surface.
11353 for expected in [
11354 CaixaKind::Biblioteca,
11355 CaixaKind::Binario,
11356 CaixaKind::Servico,
11357 CaixaKind::Supervisor,
11358 CaixaKind::Aplicacao,
11359 ] {
11360 for actual in [
11361 CaixaKind::Biblioteca,
11362 CaixaKind::Binario,
11363 CaixaKind::Servico,
11364 CaixaKind::Supervisor,
11365 CaixaKind::Aplicacao,
11366 ] {
11367 let c = caixa_with_kind(actual);
11368 let result = crate::render::require_kind(&c, expected);
11369 if expected == actual {
11370 assert!(
11371 result.is_ok(),
11372 "require_kind must accept when actual == expected \
11373 (actual={actual:?}, expected={expected:?})",
11374 );
11375 } else {
11376 let err = result.expect_err("require_kind must reject when actual != expected");
11377 assert_eq!(
11378 err.actual,
11379 c.kind(),
11380 "KindMismatch.actual must byte-equal Caixa::kind() \
11381 — the error carrier's `actual:` field reads \
11382 through the lifted accessor",
11383 );
11384 assert_eq!(
11385 err.expected, expected,
11386 "KindMismatch.expected must byte-equal the \
11387 expected variant passed to require_kind",
11388 );
11389 }
11390 }
11391 }
11392 }
11393
11394 #[test]
11395 fn aplicacao_view_kind_gate_routes_through_accessor() {
11396 // Composition pin: [`Caixa::aplicacao_view`]'s kind-gate arm
11397 // must key off [`Caixa::kind`], not the raw `.kind` field
11398 // access. Structurally: a `Caixa { kind: X, .. }` for any
11399 // non-`Aplicacao` variant must fold to `None` on the
11400 // `aplicacao_view` composer (the "kind mismatch → no typed
11401 // view" contract every downstream Aplicacao consumer keys off
11402 // via `?`), and a `Caixa { kind: Aplicacao, .. }` must fold to
11403 // `Some(_)`. The pair jointly pins the accessor + view-gate
11404 // composition: any future silent detour that had the accessor
11405 // return a fresh [`CaixaKind::Aplicacao`] on some sentinel
11406 // input would silently absorb the kind-mismatch case at the
11407 // accessor boundary and every per-Aplicacao renderer would
11408 // silently render a non-Aplicacao caixa's mesh slots — the
11409 // composition pin catches that at caixa-core build time.
11410 //
11411 // Peer of the sibling per-`Caixa`
11412 // `validate_nome_empty_arm_routes_through_accessor` (e6b7d97) /
11413 // `validate_versao_empty_arm_routes_through_accessor` (20c0539)
11414 // composition pins on the sibling outer top-level [`Caixa`]
11415 // required-`&str` universal-axis surfaces — same "the
11416 // composer / validate gate must route through the substrate-
11417 // primitive typed dispatch" discipline extended onto the
11418 // outer top-level [`Caixa`] `Copy`-return required-
11419 // discriminant composition axis.
11420 for kind in [
11421 CaixaKind::Biblioteca,
11422 CaixaKind::Binario,
11423 CaixaKind::Servico,
11424 CaixaKind::Supervisor,
11425 ] {
11426 let c = caixa_with_kind(kind);
11427 assert!(
11428 c.aplicacao_view().is_none(),
11429 "aplicacao_view must return None on non-Aplicacao \
11430 kind {kind:?} — the composer's kind-gate must route \
11431 through Caixa::kind()",
11432 );
11433 }
11434 let c = caixa_with_kind(CaixaKind::Aplicacao);
11435 assert!(
11436 c.aplicacao_view().is_some(),
11437 "aplicacao_view must return Some on kind Aplicacao — \
11438 the composer's kind-gate must accept the matching arm \
11439 through Caixa::kind()",
11440 );
11441 }
11442
11443 #[test]
11444 fn supervisor_view_kind_gate_routes_through_accessor() {
11445 // Composition pin (mirror of the sibling
11446 // `aplicacao_view_kind_gate_routes_through_accessor` on the
11447 // second `_view` composer): [`Caixa::supervisor_view`]'s kind-
11448 // gate arm must key off [`Caixa::kind`], not the raw `.kind`
11449 // field access. A `Caixa { kind: X, .. }` for any non-
11450 // `Supervisor` variant must fold to `None` on the
11451 // `supervisor_view` composer, and a `Caixa { kind:
11452 // Supervisor, .. }` must fold to `Some(_)`. Same peer
11453 // composition pin discipline on the second `_view` composer
11454 // axis.
11455 for kind in [
11456 CaixaKind::Biblioteca,
11457 CaixaKind::Binario,
11458 CaixaKind::Servico,
11459 CaixaKind::Aplicacao,
11460 ] {
11461 let c = caixa_with_kind(kind);
11462 assert!(
11463 c.supervisor_view().is_none(),
11464 "supervisor_view must return None on non-Supervisor \
11465 kind {kind:?} — the composer's kind-gate must route \
11466 through Caixa::kind()",
11467 );
11468 }
11469 let mut c = caixa_with_kind(CaixaKind::Supervisor);
11470 // A Supervisor caixa needs a strategy + at least one child to
11471 // fold to a Some(_) that also validates; the composer itself
11472 // requires only the kind arm, so bare kind flip is enough to
11473 // pin the `Some(_)` return, but we populate the minimum
11474 // supervisor shape so a future strengthening of the composer
11475 // to reject an empty spec doesn't false-positive this pin.
11476 c.estrategia = Some(crate::supervisor::RestartStrategy::OneForOne);
11477 c.children = vec![crate::supervisor::ChildSpec {
11478 caixa: "child".into(),
11479 versao: "^0.1".into(),
11480 restart: crate::supervisor::RestartPolicy::Permanent,
11481 }];
11482 assert!(
11483 c.supervisor_view().is_some(),
11484 "supervisor_view must return Some on kind Supervisor — \
11485 the composer's kind-gate must accept the matching arm \
11486 through Caixa::kind()",
11487 );
11488 }
11489
11490 #[test]
11491 fn kind_projects_by_copy() {
11492 // The by-`Copy` pin: [`Caixa::kind`] returns a fresh
11493 // [`CaixaKind`] by `Copy` — the accessor must not borrow from
11494 // `&self` (the returned value is owned, `Copy`-projected from
11495 // the underlying [`CaixaKind`] storage; two calls on the same
11496 // [`Caixa`] must yield byte-equal values). Peer of the peer
11497 // per-`:placement` `Placement::estrategia` / per-`:supervisor`
11498 // `SupervisorSpec::estrategia` / per-`:children`
11499 // `ChildSpec::restart` `Copy`-return discriminant accessor
11500 // pins on the sibling nested-spec typed-slot discriminator
11501 // axes, extended onto the first outer top-level [`Caixa`]
11502 // required-`Copy`-return axis — pins against a future silent
11503 // detour that returned `&CaixaKind` (which would type-check
11504 // but silently constrain every consumer's callsite to a
11505 // borrow-shaped dispatch, breaking the zero-cost `Copy`
11506 // projection every peer sibling accessor carries).
11507 for kind in [
11508 CaixaKind::Biblioteca,
11509 CaixaKind::Binario,
11510 CaixaKind::Servico,
11511 CaixaKind::Supervisor,
11512 CaixaKind::Aplicacao,
11513 ] {
11514 let c = caixa_with_kind(kind);
11515 let first: CaixaKind = c.kind();
11516 let second: CaixaKind = c.kind();
11517 assert_eq!(
11518 first, second,
11519 "Caixa::kind must be idempotent — two successive \
11520 calls on the same &self must return the same \
11521 CaixaKind variant",
11522 );
11523 assert_eq!(
11524 first, kind,
11525 "Caixa::kind must return :kind verbatim by Copy — \
11526 got {first:?}, expected {kind:?}",
11527 );
11528 }
11529 }
11530
11531 // ── Caixa::autores — outer top-level &[T] slice accessor ──────────
11532
11533 #[test]
11534 fn autores_returns_autores_slice_verbatim_across_permutations() {
11535 // The canonical per-`Caixa` `:autores` universal-axis maintainer-
11536 // name-list slice pin: [`Caixa::autores`] must return the
11537 // `:autores` typed [`Vec<String>`] list verbatim as a
11538 // `&[String]`, byte-equal to the raw `self.autores.as_slice()`
11539 // access across every representative value in the accept-set —
11540 // `[]` (the "no maintainers declared" arm every existing
11541 // fixture without an `:autores` line carries), `[""]` (a past-
11542 // the-guard sentinel that pins the accessor doesn't perform a
11543 // silent `[""] → []` collapse on the empty-entry arm — validate
11544 // rejects `[""]` through `AutorEmpty` but the accessor must
11545 // ship the raw slot verbatim so a validate-time gate regression
11546 // surfaces at the caixa-helm emit boundary rather than being
11547 // silently absorbed into a maintainer-drop), `["pleme-io"]` (the
11548 // canonical single-maintainer form every `feira init` template
11549 // scaffolds), `["alice", "bob"]` (a canonical multi-maintainer
11550 // form), `["alice <alice@example.com>", "bob <bob@example.com>"]`
11551 // (the canonical RFC-5322 `<name> <email>` form the
11552 // `is_chart_maintainer_name_shape` predicate accepts), and
11553 // `["pleme-io", "pleme-io"]` (a past-the-guard duplicate
11554 // sentinel — validate rejects through `AutorDuplicate` but the
11555 // accessor must ship the raw slot verbatim).
11556 //
11557 // First outer top-level [`Caixa`] `&[T]`-return slice accessor
11558 // pin on the substrate primitive — opens the "outer [`Caixa`]
11559 // `&[T]` slice" projection pattern the sibling per-`Caixa`
11560 // `:etiquetas` / `:deps` / `:deps-dev` / `:exe` / `:bibliotecas`
11561 // / `:servicos` / `:upgrade-from` / `:children` future lifts
11562 // fold on. Sibling in shape to the peer per-`:supervisor`
11563 // [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
11564 // per-`:placement` [`crate::aplicacao::Placement::clusters`]
11565 // (a6e18d7), per-`:membros`
11566 // [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
11567 // per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
11568 // (0dcc926), and per-`:upgrade-from :instructions`
11569 // [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
11570 // `&[T]`-return slice accessor pins on the sibling per-M2 /
11571 // per-M3 typed-slot list axes, extended onto the outer top-
11572 // level [`Caixa`] universal-axis surface. Pins against a future
11573 // silent detour that returned an owned `Vec<String>` (which
11574 // would type-check but silently clone on every accessor call,
11575 // breaking the zero-cost projection every peer sibling slice
11576 // accessor carries), a `[""] → []` collapse (which would
11577 // silently absorb the `AutorEmpty` refusal case at the accessor
11578 // boundary), or a `["a", "a"] → ["a"]` dedup collapse (which
11579 // would silently absorb the `AutorDuplicate` refusal case at
11580 // the accessor boundary and the caixa-helm `maintainers:` fold
11581 // would silently render a dedupped list on a struct-literal
11582 // `Caixa { autores: vec!["a".into(), "a".into()], .. }`).
11583 for autores in [
11584 vec![],
11585 vec![""],
11586 vec!["pleme-io"],
11587 vec!["alice", "bob"],
11588 vec!["alice <alice@example.com>", "bob <bob@example.com>"],
11589 vec!["pleme-io", "pleme-io"],
11590 ] {
11591 let c = caixa_with_autores(autores.clone());
11592 let expected: Vec<String> = autores.iter().map(|s| (*s).to_string()).collect();
11593 assert_eq!(
11594 c.autores(),
11595 expected.as_slice(),
11596 "Caixa::autores must return :autores verbatim (got {:?}, \
11597 expected {expected:?})",
11598 c.autores(),
11599 );
11600 assert_eq!(
11601 c.autores(),
11602 c.autores.as_slice(),
11603 "Caixa::autores must byte-equal the raw \
11604 `self.autores.as_slice()` field access across every \
11605 value in the Vec<String> accept-set",
11606 );
11607 }
11608 }
11609
11610 #[test]
11611 fn validate_autores_empty_entry_arm_routes_through_accessor() {
11612 // Composition pin: [`Caixa::validate_autores`]'s per-entry
11613 // empty-arm gate must key off [`Caixa::autores`], not the raw
11614 // `&self.autores` field-borrow walk. Structurally: a
11615 // `Caixa { autores: vec!["".into()], .. }` must surface the
11616 // `AutorEmpty` refusal exactly, and a
11617 // `Caixa { autores: vec!["pleme-io".into()], .. }` (the
11618 // canonical single-maintainer form) must pass validate. The
11619 // pair jointly pins the accessor + validate-gate composition:
11620 // any future silent detour that had the accessor return an
11621 // empty slice on the `[""]` arm (a
11622 // `.iter().filter(|s| !s.is_empty()).collect()` collapse)
11623 // would silently absorb the `AutorEmpty` refusal at the
11624 // accessor boundary and the validate gate would accept a
11625 // struct-literal `Caixa { autores: vec!["".into()], .. }` —
11626 // the composition pin catches that at caixa-core build time.
11627 //
11628 // Peer of the per-`Caixa` [`Caixa::validate_licenca`] (6d5bc28)
11629 // accessor-composition pin
11630 // (`validate_licenca_empty_arm_routes_through_accessor`) on the
11631 // sibling `Option<&str>`-composition axis and the
11632 // per-`:politicas :circuit-breaker`
11633 // [`crate::aplicacao::CircuitBreaker::max_failures`] (3a74062)
11634 // accessor-composition pin
11635 // (`validate_politicas_max_failures_zero_floor_arm_routes_through_accessor`)
11636 // on the sibling required-`u32`-composition axis — same "the
11637 // validate / shape-gate predicate must route through the
11638 // substrate-primitive typed dispatch" discipline extended onto
11639 // the outer top-level [`Caixa`] universal-axis `&[T]`-
11640 // composition surface.
11641 let c = caixa_with_autores(vec![""]);
11642 assert!(
11643 matches!(c.validate_autores(), Err(ManifestError::AutorEmpty)),
11644 "validate_autores must reject autores == vec![\"\"] with \
11645 AutorEmpty — the accessor and the validate gate must \
11646 route through the same substrate-primitive typed dispatch \
11647 on the :autores per-entry empty arm",
11648 );
11649 let c = caixa_with_autores(vec!["pleme-io"]);
11650 assert!(
11651 c.validate_autores().is_ok(),
11652 "validate_autores must accept autores == vec![\"pleme-io\"] \
11653 (the canonical single-maintainer shape every `feira init` \
11654 template scaffolds)",
11655 );
11656 }
11657
11658 #[test]
11659 fn autores_projects_slice_by_borrow() {
11660 // The by-borrow pin: [`Caixa::autores`] returns `&[String]` by
11661 // borrow — the returned slice borrows the underlying
11662 // `Vec<String>` storage of the `:autores` slot and the
11663 // accessor must not clone the backing `Vec` on every call.
11664 // Peer of the per-`:membros`
11665 // [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36) /
11666 // per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
11667 // (0dcc926) / per-`:placement`
11668 // [`crate::aplicacao::Placement::clusters`] (a6e18d7) /
11669 // per-`:supervisor` [`crate::supervisor::SupervisorSpec::children`]
11670 // (bc92bce) by-borrow pins on the sibling per-M2 / per-M3
11671 // typed-slot `&[T]`-return axes, extended onto the outer top-
11672 // level [`Caixa`] universal-axis `&[String]` shape — the
11673 // accessor's returned slice must borrow from `&self` (the
11674 // returned reference's lifetime is tied to `&self`), and
11675 // calling the accessor twice on the same [`Caixa`] must yield
11676 // slices that are pointer-equal (the underlying byte-buffer is
11677 // the storage `Vec`'s allocation, not a fresh copy) as well as
11678 // value-equal (idempotent, no side effects on `&self`).
11679 //
11680 // Pins against a future silent detour that returned an owned
11681 // `Vec<String>` (which would type-check but silently clone on
11682 // every call, breaking the zero-cost projection every peer
11683 // sibling slice accessor carries), a `&Vec<String>` return
11684 // (which would leak the backing `Vec`'s grow/push/reserve
11685 // surface no downstream consumer reaches for), or a one-arm-
11686 // only accessor that returned a saturating value on some
11687 // sentinel input (breaking the pass-through invariant the
11688 // sibling slice accessors carry).
11689 for autores in [
11690 vec![],
11691 vec!["pleme-io"],
11692 vec!["alice", "bob"],
11693 vec!["pleme-io", "pleme-io"],
11694 ] {
11695 let c = caixa_with_autores(autores.clone());
11696 let expected: Vec<String> = autores.iter().map(|s| (*s).to_string()).collect();
11697 let first = c.autores();
11698 let second = c.autores();
11699 assert_eq!(
11700 first, second,
11701 "Caixa::autores must be idempotent — two successive \
11702 calls on the same &self must return the same \
11703 &[String]",
11704 );
11705 assert_eq!(
11706 first.as_ptr(),
11707 second.as_ptr(),
11708 "Caixa::autores must borrow the underlying Vec<String> \
11709 storage — two successive calls must return slices \
11710 with the same backing pointer (a fresh Vec<String> \
11711 clone would change the pointer on every call)",
11712 );
11713 assert_eq!(
11714 first,
11715 expected.as_slice(),
11716 "Caixa::autores must return :autores verbatim by \
11717 borrow — got {first:?}, expected {expected:?}",
11718 );
11719 }
11720 }
11721
11722 // ── Caixa::etiquetas — outer top-level &[T] slice accessor ────────
11723
11724 #[test]
11725 fn etiquetas_returns_etiquetas_slice_verbatim_across_permutations() {
11726 // The canonical per-`Caixa` `:etiquetas` universal-axis
11727 // registry-search-tag-list slice pin: [`Caixa::etiquetas`] must
11728 // return the `:etiquetas` typed [`Vec<String>`] list verbatim
11729 // as a `&[String]`, byte-equal to the raw
11730 // `self.etiquetas.as_slice()` access across every representative
11731 // value in the accept-set — `[]` (the "no tags declared" arm
11732 // every existing fixture without an `:etiquetas` line carries),
11733 // `[""]` (a past-the-guard sentinel that pins the accessor
11734 // doesn't perform a silent `[""] → []` collapse on the empty-
11735 // entry arm — validate rejects `[""]` through `EtiquetaEmpty`
11736 // but the accessor must ship the raw slot verbatim so a
11737 // validate-time gate regression surfaces at the caixa-helm emit
11738 // boundary rather than being silently absorbed into a keyword-
11739 // drop), `["demo"]` (the canonical single-tag form every
11740 // `feira init` template scaffolds), `["example", "aplicacao",
11741 // "mesh", "ecommerce", "demo"]` (the canonical multi-tag form
11742 // the checkout-aplicacao fixture emits), and `["demo", "demo"]`
11743 // (a past-the-guard duplicate sentinel — validate rejects
11744 // through `EtiquetaDuplicate` but the accessor must ship the
11745 // raw slot verbatim so the caixa-helm `BTreeSet::collect` dedup
11746 // at chart-render time isn't silently promoted into the
11747 // accessor boundary and struct-literal
11748 // `Caixa { etiquetas: vec!["demo".into(), "demo".into()], .. }`
11749 // fixtures continue to expose the duplicate at the accessor).
11750 //
11751 // Second outer top-level [`Caixa`] `&[T]`-return slice accessor
11752 // pin on the substrate primitive — folds on the "outer
11753 // [`Caixa`] `&[T]` slice" projection pattern
11754 // `autores_returns_autores_slice_verbatim_across_permutations`
11755 // (b5d813f) opened, sibling in shape and idiom. Pins against a
11756 // future silent detour that returned an owned `Vec<String>`
11757 // (which would type-check but silently clone on every accessor
11758 // call, breaking the zero-cost projection every peer sibling
11759 // slice accessor carries), a `[""] → []` collapse (which would
11760 // silently absorb the `EtiquetaEmpty` refusal case at the
11761 // accessor boundary), or a `["a", "a"] → ["a"]` dedup collapse
11762 // (which would silently absorb the `EtiquetaDuplicate` refusal
11763 // case at the accessor boundary — the caixa-helm chart-render
11764 // `BTreeSet::collect` dedup is downstream of the accessor and
11765 // must not be silently promoted into it).
11766 for etiquetas in [
11767 vec![],
11768 vec![""],
11769 vec!["demo"],
11770 vec!["example", "aplicacao", "mesh", "ecommerce", "demo"],
11771 vec!["demo", "demo"],
11772 ] {
11773 let c = caixa_with_etiquetas(etiquetas.clone());
11774 let expected: Vec<String> = etiquetas.iter().map(|s| (*s).to_string()).collect();
11775 assert_eq!(
11776 c.etiquetas(),
11777 expected.as_slice(),
11778 "Caixa::etiquetas must return :etiquetas verbatim (got \
11779 {:?}, expected {expected:?})",
11780 c.etiquetas(),
11781 );
11782 assert_eq!(
11783 c.etiquetas(),
11784 c.etiquetas.as_slice(),
11785 "Caixa::etiquetas must byte-equal the raw \
11786 `self.etiquetas.as_slice()` field access across every \
11787 value in the Vec<String> accept-set",
11788 );
11789 }
11790 }
11791
11792 #[test]
11793 fn validate_etiquetas_empty_entry_arm_routes_through_accessor() {
11794 // Composition pin: [`Caixa::validate_etiquetas`]'s per-entry
11795 // empty-arm gate must key off [`Caixa::etiquetas`], not the raw
11796 // `&self.etiquetas` field-borrow walk. Structurally: a
11797 // `Caixa { etiquetas: vec!["".into()], .. }` must surface the
11798 // `EtiquetaEmpty` refusal exactly, and a
11799 // `Caixa { etiquetas: vec!["demo".into()], .. }` (the canonical
11800 // single-tag form) must pass validate. The pair jointly pins
11801 // the accessor + validate-gate composition: any future silent
11802 // detour that had the accessor return an empty slice on the
11803 // `[""]` arm (a
11804 // `.iter().filter(|s| !s.is_empty()).collect()` collapse) would
11805 // silently absorb the `EtiquetaEmpty` refusal at the accessor
11806 // boundary and the validate gate would accept a struct-literal
11807 // `Caixa { etiquetas: vec!["".into()], .. }` — the composition
11808 // pin catches that at caixa-core build time.
11809 //
11810 // Peer of the per-`Caixa` `validate_autores_empty_arm_routes_
11811 // through_accessor` (b5d813f) accessor-composition pin on the
11812 // sibling `&[T]`-composition axis — same "the validate / shape-
11813 // gate predicate must route through the substrate-primitive
11814 // typed dispatch" discipline extended onto the sibling outer
11815 // top-level [`Caixa`] `&[T]`-composition surface.
11816 let c = caixa_with_etiquetas(vec![""]);
11817 assert!(
11818 matches!(c.validate_etiquetas(), Err(ManifestError::EtiquetaEmpty)),
11819 "validate_etiquetas must reject etiquetas == vec![\"\"] \
11820 with EtiquetaEmpty — the accessor and the validate gate \
11821 must route through the same substrate-primitive typed \
11822 dispatch on the :etiquetas per-entry empty arm",
11823 );
11824 let c = caixa_with_etiquetas(vec!["demo"]);
11825 assert!(
11826 c.validate_etiquetas().is_ok(),
11827 "validate_etiquetas must accept etiquetas == vec![\"demo\"] \
11828 (the canonical single-tag shape every `feira init` \
11829 template scaffolds)",
11830 );
11831 }
11832
11833 #[test]
11834 fn etiquetas_projects_slice_by_borrow() {
11835 // The by-borrow pin: [`Caixa::etiquetas`] returns `&[String]`
11836 // by borrow — the returned slice borrows the underlying
11837 // `Vec<String>` storage of the `:etiquetas` slot and the
11838 // accessor must not clone the backing `Vec` on every call.
11839 // Peer of the per-`Caixa` `autores_projects_slice_by_borrow`
11840 // (b5d813f) by-borrow pin on the sibling outer top-level
11841 // [`Caixa`] `&[String]`-return axis — the accessor's returned
11842 // slice must borrow from `&self` (the returned reference's
11843 // lifetime is tied to `&self`), and calling the accessor twice
11844 // on the same [`Caixa`] must yield slices that are pointer-
11845 // equal (the underlying byte-buffer is the storage `Vec`'s
11846 // allocation, not a fresh copy) as well as value-equal
11847 // (idempotent, no side effects on `&self`).
11848 //
11849 // Pins against a future silent detour that returned an owned
11850 // `Vec<String>` (which would type-check but silently clone on
11851 // every call, breaking the zero-cost projection every peer
11852 // sibling slice accessor carries), a `&Vec<String>` return
11853 // (which would leak the backing `Vec`'s grow/push/reserve
11854 // surface no downstream consumer reaches for), or a one-arm-
11855 // only accessor that returned a saturating value on some
11856 // sentinel input (breaking the pass-through invariant the
11857 // sibling slice accessors carry).
11858 for etiquetas in [
11859 vec![],
11860 vec!["demo"],
11861 vec!["example", "aplicacao", "mesh"],
11862 vec!["demo", "demo"],
11863 ] {
11864 let c = caixa_with_etiquetas(etiquetas.clone());
11865 let expected: Vec<String> = etiquetas.iter().map(|s| (*s).to_string()).collect();
11866 let first = c.etiquetas();
11867 let second = c.etiquetas();
11868 assert_eq!(
11869 first, second,
11870 "Caixa::etiquetas must be idempotent — two successive \
11871 calls on the same &self must return the same \
11872 &[String]",
11873 );
11874 assert_eq!(
11875 first.as_ptr(),
11876 second.as_ptr(),
11877 "Caixa::etiquetas must borrow the underlying \
11878 Vec<String> storage — two successive calls must \
11879 return slices with the same backing pointer (a fresh \
11880 Vec<String> clone would change the pointer on every \
11881 call)",
11882 );
11883 assert_eq!(
11884 first,
11885 expected.as_slice(),
11886 "Caixa::etiquetas must return :etiquetas verbatim by \
11887 borrow — got {first:?}, expected {expected:?}",
11888 );
11889 }
11890 }
11891
11892 // ── Caixa::bibliotecas — outer top-level &[T] slice accessor ──────
11893
11894 #[test]
11895 fn bibliotecas_returns_bibliotecas_slice_verbatim_across_permutations() {
11896 // The canonical per-`Caixa` `:bibliotecas` universal-axis
11897 // library-source-path-list slice pin: [`Caixa::bibliotecas`]
11898 // must return the `:bibliotecas` typed [`Vec<String>`] list
11899 // verbatim as a `&[String]`, byte-equal to the raw
11900 // `self.bibliotecas.as_slice()` access across every
11901 // representative value in the accept-set — `[]` (the "no
11902 // libraries declared" arm every `:kind` other than `Biblioteca`
11903 // + every `Biblioteca` relying on the canonical
11904 // `lib/<nome>.lisp` implicit-default path carries; the
11905 // layout's [`crate::LayoutInvariants`] `MissingLib` arm-gate
11906 // fires exactly on this empty-slot + `Biblioteca`-kind
11907 // combination), `[""]` (a past-the-guard sentinel that pins
11908 // the accessor doesn't perform a silent `[""] → []` collapse
11909 // on the empty-entry arm — validate rejects `[""]` through
11910 // `CodePathEmpty { slot: ":bibliotecas" }` but the accessor
11911 // must ship the raw slot verbatim so a validate-time gate
11912 // regression surfaces at the `feira build` phase-1 parse
11913 // boundary rather than being silently absorbed into a
11914 // library-drop), `["lib/demo.lisp"]` (the canonical single-
11915 // entry form `Caixa::template` scaffolds and every `feira init`
11916 // template emits), `["lib/demo.lisp", "lib/helpers.lisp"]`
11917 // (the canonical multi-library form the
11918 // `validate_code_paths_accepts_explicit_relative_paths_on_
11919 // every_slot` fixture emits), and `["lib/foo.lisp",
11920 // "lib/foo.lisp"]` (a past-the-guard duplicate sentinel —
11921 // validate rejects through `CodePathDuplicate { slot:
11922 // ":bibliotecas" }` per the per-slot set-not-multiset gate,
11923 // but the accessor must ship the raw slot verbatim so the
11924 // `feira build` `for entry in caixa.bibliotecas()` parse walk
11925 // sees the duplicate at the accessor boundary and struct-
11926 // literal `Caixa { bibliotecas: vec!["lib/foo.lisp".into(),
11927 // "lib/foo.lisp".into()], .. }` fixtures continue to expose
11928 // the duplicate at the accessor).
11929 //
11930 // Third outer top-level [`Caixa`] `&[T]`-return slice accessor
11931 // pin on the substrate primitive — folds on the "outer
11932 // [`Caixa`] `&[T]` slice" projection pattern
11933 // `autores_returns_autores_slice_verbatim_across_permutations`
11934 // (b5d813f) opened and
11935 // `etiquetas_returns_etiquetas_slice_verbatim_across_permutations`
11936 // (78c7d3c) folded on, sibling in shape and idiom. Pins
11937 // against a future silent detour that returned an owned
11938 // `Vec<String>` (which would type-check but silently clone on
11939 // every accessor call, breaking the zero-cost projection
11940 // every peer sibling slice accessor carries), a `[""] → []`
11941 // collapse (which would silently absorb the `CodePathEmpty`
11942 // refusal case at the accessor boundary), or a `["lib/foo.lisp",
11943 // "lib/foo.lisp"] → ["lib/foo.lisp"]` dedup collapse (which
11944 // would silently absorb the `CodePathDuplicate` refusal case
11945 // at the accessor boundary — the per-slot set-not-multiset
11946 // gate is downstream of the accessor and must not be silently
11947 // promoted into it).
11948 for bibliotecas in [
11949 vec![],
11950 vec![""],
11951 vec!["lib/demo.lisp"],
11952 vec!["lib/demo.lisp", "lib/helpers.lisp"],
11953 vec!["lib/foo.lisp", "lib/foo.lisp"],
11954 ] {
11955 let c = caixa_with_code_paths(bibliotecas.clone(), vec![], vec![]);
11956 let expected: Vec<String> = bibliotecas.iter().map(|s| (*s).to_string()).collect();
11957 assert_eq!(
11958 c.bibliotecas(),
11959 expected.as_slice(),
11960 "Caixa::bibliotecas must return :bibliotecas verbatim \
11961 (got {:?}, expected {expected:?})",
11962 c.bibliotecas(),
11963 );
11964 assert_eq!(
11965 c.bibliotecas(),
11966 c.bibliotecas.as_slice(),
11967 "Caixa::bibliotecas must byte-equal the raw \
11968 `self.bibliotecas.as_slice()` field access across \
11969 every value in the Vec<String> accept-set",
11970 );
11971 }
11972 }
11973
11974 #[test]
11975 fn validate_code_paths_bibliotecas_empty_arm_routes_through_accessor() {
11976 // Composition pin: [`Caixa::validate_code_paths`]'s per-entry
11977 // empty-arm gate on the `:bibliotecas` slot must key off
11978 // [`Caixa::bibliotecas`], not a divergent raw
11979 // `&self.bibliotecas` field-borrow walk. Structurally: a
11980 // `Caixa { bibliotecas: vec!["".into()], .. }` must surface
11981 // the `CodePathEmpty { slot: ":bibliotecas" }` refusal
11982 // exactly, and a `Caixa { bibliotecas: vec!["lib/demo.lisp".
11983 // into()], .. }` (the canonical single-library form
11984 // `Caixa::template` scaffolds) must pass validate. The pair
11985 // jointly pins the accessor + validate-gate composition: any
11986 // future silent detour that had the accessor return an empty
11987 // slice on the `[""]` arm (a `.iter().filter(|s|
11988 // !s.is_empty()).collect()` collapse) would silently absorb
11989 // the `CodePathEmpty` refusal at the accessor boundary and
11990 // the validate gate would accept a struct-literal
11991 // `Caixa { bibliotecas: vec!["".into()], .. }` — the
11992 // composition pin catches that at caixa-core build time.
11993 //
11994 // Peer of the per-`Caixa` `validate_autores_empty_arm_routes_
11995 // through_accessor` (b5d813f) and
11996 // `validate_etiquetas_empty_entry_arm_routes_through_accessor`
11997 // (78c7d3c) accessor-composition pins on the sibling `&[T]`-
11998 // composition axes — same "the validate / shape-gate
11999 // predicate must route through the substrate-primitive typed
12000 // dispatch" discipline extended onto the sibling outer top-
12001 // level [`Caixa`] `&[T]`-composition surface. Nominally the
12002 // in-tree `validate_code_paths` production body still keys
12003 // off the internal `[(":bibliotecas", &self.bibliotecas,
12004 // CodePathFileType::LispSource), (":exe", &self.exe, ..),
12005 // (":servicos", &self.servicos, ..)]` per-slot dispatch tuple
12006 // (the tuple's homogeneous slice-typed shape blocks a per-
12007 // element accessor swap in isolation — a future companion
12008 // lift for `:exe` and `:servicos` on the same outer-`Caixa`
12009 // `&[T]` slice-accessor axis closes that tuple onto the
12010 // triple of typed dispatches as a unit); the composition pin
12011 // catches any future accessor-side silent filter drop against
12012 // that eventual tuple-closure regardless of whether the
12013 // `:bibliotecas` slot is threaded through the accessor or the
12014 // raw field access at the tuple's construction site.
12015 let c = caixa_with_code_paths(vec![""], vec![], vec![]);
12016 assert!(
12017 matches!(
12018 c.validate_code_paths(),
12019 Err(ManifestError::CodePathEmpty {
12020 slot: ":bibliotecas"
12021 })
12022 ),
12023 "validate_code_paths must reject bibliotecas == vec![\"\"] \
12024 with CodePathEmpty {{ slot: \":bibliotecas\" }} — the \
12025 accessor and the validate gate must route through the \
12026 same substrate-primitive typed dispatch on the \
12027 :bibliotecas per-entry empty arm",
12028 );
12029 let c = caixa_with_code_paths(vec!["lib/demo.lisp"], vec![], vec![]);
12030 assert!(
12031 c.validate_code_paths().is_ok(),
12032 "validate_code_paths must accept bibliotecas == \
12033 vec![\"lib/demo.lisp\"] (the canonical single-library \
12034 shape every `feira init` template scaffolds)",
12035 );
12036 }
12037
12038 #[test]
12039 fn bibliotecas_projects_slice_by_borrow() {
12040 // The by-borrow pin: [`Caixa::bibliotecas`] returns
12041 // `&[String]` by borrow — the returned slice borrows the
12042 // underlying `Vec<String>` storage of the `:bibliotecas` slot
12043 // and the accessor must not clone the backing `Vec` on every
12044 // call. Peer of the per-`Caixa` `autores_projects_slice_by_borrow`
12045 // (b5d813f) and `etiquetas_projects_slice_by_borrow` (78c7d3c)
12046 // by-borrow pins on the sibling outer top-level [`Caixa`]
12047 // `&[String]`-return axes — the accessor's returned slice
12048 // must borrow from `&self` (the returned reference's lifetime
12049 // is tied to `&self`), and calling the accessor twice on the
12050 // same [`Caixa`] must yield slices that are pointer-equal
12051 // (the underlying byte-buffer is the storage `Vec`'s
12052 // allocation, not a fresh copy) as well as value-equal
12053 // (idempotent, no side effects on `&self`).
12054 //
12055 // Pins against a future silent detour that returned an owned
12056 // `Vec<String>` (which would type-check but silently clone on
12057 // every call, breaking the zero-cost projection every peer
12058 // sibling slice accessor carries), a `&Vec<String>` return
12059 // (which would leak the backing `Vec`'s grow/push/reserve
12060 // surface no downstream consumer reaches for), or a one-arm-
12061 // only accessor that returned a saturating value on some
12062 // sentinel input (breaking the pass-through invariant the
12063 // sibling slice accessors carry).
12064 for bibliotecas in [
12065 vec![],
12066 vec!["lib/demo.lisp"],
12067 vec!["lib/demo.lisp", "lib/helpers.lisp"],
12068 vec!["lib/foo.lisp", "lib/foo.lisp"],
12069 ] {
12070 let c = caixa_with_code_paths(bibliotecas.clone(), vec![], vec![]);
12071 let expected: Vec<String> = bibliotecas.iter().map(|s| (*s).to_string()).collect();
12072 let first = c.bibliotecas();
12073 let second = c.bibliotecas();
12074 assert_eq!(
12075 first, second,
12076 "Caixa::bibliotecas must be idempotent — two \
12077 successive calls on the same &self must return the \
12078 same &[String]",
12079 );
12080 assert_eq!(
12081 first.as_ptr(),
12082 second.as_ptr(),
12083 "Caixa::bibliotecas must borrow the underlying \
12084 Vec<String> storage — two successive calls must \
12085 return slices with the same backing pointer (a \
12086 fresh Vec<String> clone would change the pointer on \
12087 every call)",
12088 );
12089 assert_eq!(
12090 first,
12091 expected.as_slice(),
12092 "Caixa::bibliotecas must return :bibliotecas verbatim \
12093 by borrow — got {first:?}, expected {expected:?}",
12094 );
12095 }
12096 }
12097
12098 // ── Caixa::exe — outer top-level &[T] slice accessor ──────────────
12099
12100 #[test]
12101 fn exe_returns_exe_slice_verbatim_across_permutations() {
12102 // The canonical per-`Caixa` `:exe` universal-axis
12103 // nix-built-executable-entry-path-list slice pin: [`Caixa::exe`]
12104 // must return the `:exe` typed [`Vec<String>`] list verbatim as
12105 // a `&[String]`, byte-equal to the raw `self.exe.as_slice()`
12106 // access across every representative value in the accept-set —
12107 // `[]` (the "no executable declared" arm every `:kind` other
12108 // than `Binario` carries; the layout's [`crate::LayoutInvariants`]
12109 // `BinarioWithoutExe` arm-gate fires exactly on this empty-slot
12110 // + `Binario`-kind combination), `[""]` (a past-the-guard
12111 // sentinel that pins the accessor doesn't perform a silent
12112 // `[""] → []` collapse on the empty-entry arm — validate rejects
12113 // `[""]` through `CodePathEmpty { slot: ":exe" }` but the
12114 // accessor must ship the raw slot verbatim so a validate-time
12115 // gate regression surfaces at the layout / `feira nix` boundary
12116 // rather than being silently absorbed into an executable-drop),
12117 // `["exe/cli"]` (the canonical single-entry Binario form every
12118 // in-tree `caixa_with_code_paths` positive control uses),
12119 // `["exe/cli", "exe/serve"]` (the canonical multi-executable
12120 // form the `validate_code_paths_accepts_explicit_relative_paths_
12121 // on_every_slot` fixture emits), and `["exe/cli", "exe/cli"]`
12122 // (a past-the-guard duplicate sentinel — validate rejects
12123 // through `CodePathDuplicate { slot: ":exe" }` per the per-slot
12124 // set-not-multiset gate, but the accessor must ship the raw
12125 // slot verbatim so struct-literal `Caixa { exe: vec!["exe/cli".
12126 // into(), "exe/cli".into()], .. }` fixtures continue to expose
12127 // the duplicate at the accessor).
12128 //
12129 // Fourth outer top-level [`Caixa`] `&[T]`-return slice accessor
12130 // pin on the substrate primitive — folds on the "outer
12131 // [`Caixa`] `&[T]` slice" projection pattern
12132 // `autores_returns_autores_slice_verbatim_across_permutations`
12133 // (b5d813f) opened,
12134 // `etiquetas_returns_etiquetas_slice_verbatim_across_permutations`
12135 // (78c7d3c) folded on, and
12136 // `bibliotecas_returns_bibliotecas_slice_verbatim_across_permutations`
12137 // (8a36c23) closed the universal-axis text-tag family of.
12138 // Opens the outer-`Caixa` foreign-code-slot `&[T]` sub-family
12139 // the sibling `:servicos` future lift closes onto. Pins against
12140 // a future silent detour that returned an owned `Vec<String>`
12141 // (which would type-check but silently clone on every accessor
12142 // call, breaking the zero-cost projection every peer sibling
12143 // slice accessor carries), a `[""] → []` collapse (which would
12144 // silently absorb the `CodePathEmpty` refusal case at the
12145 // accessor boundary), or an `["exe/cli", "exe/cli"] →
12146 // ["exe/cli"]` dedup collapse (which would silently absorb the
12147 // `CodePathDuplicate` refusal case at the accessor boundary —
12148 // the per-slot set-not-multiset gate is downstream of the
12149 // accessor and must not be silently promoted into it).
12150 for exe in [
12151 vec![],
12152 vec![""],
12153 vec!["exe/cli"],
12154 vec!["exe/cli", "exe/serve"],
12155 vec!["exe/cli", "exe/cli"],
12156 ] {
12157 let c = caixa_with_code_paths(vec![], exe.clone(), vec![]);
12158 let expected: Vec<String> = exe.iter().map(|s| (*s).to_string()).collect();
12159 assert_eq!(
12160 c.exe(),
12161 expected.as_slice(),
12162 "Caixa::exe must return :exe verbatim (got {:?}, \
12163 expected {expected:?})",
12164 c.exe(),
12165 );
12166 assert_eq!(
12167 c.exe(),
12168 c.exe.as_slice(),
12169 "Caixa::exe must byte-equal the raw \
12170 `self.exe.as_slice()` field access across every value \
12171 in the Vec<String> accept-set",
12172 );
12173 }
12174 }
12175
12176 #[test]
12177 fn validate_code_paths_exe_empty_arm_routes_through_accessor() {
12178 // Composition pin: [`Caixa::validate_code_paths`]'s per-entry
12179 // empty-arm gate on the `:exe` slot must key off
12180 // [`Caixa::exe`], not a divergent raw `&self.exe` field-borrow
12181 // walk. Structurally: a `Caixa { exe: vec!["".into()], .. }`
12182 // must surface the `CodePathEmpty { slot: ":exe" }` refusal
12183 // exactly, and a `Caixa { exe: vec!["exe/cli".into()], .. }`
12184 // (the canonical single-executable form every in-tree
12185 // `caixa_with_code_paths` positive control uses) must pass
12186 // validate. The pair jointly pins the accessor + validate-gate
12187 // composition: any future silent detour that had the accessor
12188 // return an empty slice on the `[""]` arm (a
12189 // `.iter().filter(|s| !s.is_empty()).collect()` collapse) would
12190 // silently absorb the `CodePathEmpty` refusal at the accessor
12191 // boundary and the validate gate would accept a struct-literal
12192 // `Caixa { exe: vec!["".into()], .. }` — the composition pin
12193 // catches that at caixa-core build time.
12194 //
12195 // Peer of the per-`Caixa`
12196 // `validate_code_paths_bibliotecas_empty_arm_routes_through_accessor`
12197 // (8a36c23), `validate_autores_empty_arm_routes_through_accessor`
12198 // (b5d813f), and
12199 // `validate_etiquetas_empty_entry_arm_routes_through_accessor`
12200 // (78c7d3c) accessor-composition pins on the sibling `&[T]`-
12201 // composition axes — same "the validate / shape-gate predicate
12202 // must route through the substrate-primitive typed dispatch"
12203 // discipline extended onto the sibling outer top-level [`Caixa`]
12204 // `&[T]`-composition surface. Nominally the in-tree
12205 // `validate_code_paths` production body still keys off the
12206 // internal `[(":bibliotecas", &self.bibliotecas,
12207 // CodePathFileType::LispSource), (":exe", &self.exe, ..),
12208 // (":servicos", &self.servicos, ..)]` per-slot dispatch tuple
12209 // (the tuple's homogeneous slice-typed shape blocks a per-
12210 // element accessor swap in isolation — a future companion lift
12211 // for `:servicos` on the same outer-`Caixa` `&[T]` slice-
12212 // accessor axis closes that tuple onto the triple of typed
12213 // dispatches as a unit); the composition pin catches any future
12214 // accessor-side silent filter drop against that eventual tuple-
12215 // closure regardless of whether the `:exe` slot is threaded
12216 // through the accessor or the raw field access at the tuple's
12217 // construction site.
12218 let c = caixa_with_code_paths(vec![], vec![""], vec![]);
12219 assert!(
12220 matches!(
12221 c.validate_code_paths(),
12222 Err(ManifestError::CodePathEmpty { slot: ":exe" })
12223 ),
12224 "validate_code_paths must reject exe == vec![\"\"] \
12225 with CodePathEmpty {{ slot: \":exe\" }} — the \
12226 accessor and the validate gate must route through the \
12227 same substrate-primitive typed dispatch on the \
12228 :exe per-entry empty arm",
12229 );
12230 let c = caixa_with_code_paths(vec![], vec!["exe/cli"], vec![]);
12231 assert!(
12232 c.validate_code_paths().is_ok(),
12233 "validate_code_paths must accept exe == vec![\"exe/cli\"] \
12234 (the canonical single-executable shape every in-tree \
12235 `caixa_with_code_paths` positive control uses)",
12236 );
12237 }
12238
12239 #[test]
12240 fn exe_projects_slice_by_borrow() {
12241 // The by-borrow pin: [`Caixa::exe`] returns `&[String]` by
12242 // borrow — the returned slice borrows the underlying
12243 // `Vec<String>` storage of the `:exe` slot and the accessor
12244 // must not clone the backing `Vec` on every call. Peer of the
12245 // per-`Caixa` `autores_projects_slice_by_borrow` (b5d813f),
12246 // `etiquetas_projects_slice_by_borrow` (78c7d3c), and
12247 // `bibliotecas_projects_slice_by_borrow` (8a36c23) by-borrow
12248 // pins on the sibling outer top-level [`Caixa`] `&[String]`-
12249 // return axes — the accessor's returned slice must borrow from
12250 // `&self` (the returned reference's lifetime is tied to
12251 // `&self`), and calling the accessor twice on the same
12252 // [`Caixa`] must yield slices that are pointer-equal (the
12253 // underlying byte-buffer is the storage `Vec`'s allocation,
12254 // not a fresh copy) as well as value-equal (idempotent, no
12255 // side effects on `&self`).
12256 //
12257 // Pins against a future silent detour that returned an owned
12258 // `Vec<String>` (which would type-check but silently clone on
12259 // every call, breaking the zero-cost projection every peer
12260 // sibling slice accessor carries), a `&Vec<String>` return
12261 // (which would leak the backing `Vec`'s grow/push/reserve
12262 // surface no downstream consumer reaches for), or a one-arm-
12263 // only accessor that returned a saturating value on some
12264 // sentinel input (breaking the pass-through invariant the
12265 // sibling slice accessors carry).
12266 for exe in [
12267 vec![],
12268 vec!["exe/cli"],
12269 vec!["exe/cli", "exe/serve"],
12270 vec!["exe/cli", "exe/cli"],
12271 ] {
12272 let c = caixa_with_code_paths(vec![], exe.clone(), vec![]);
12273 let expected: Vec<String> = exe.iter().map(|s| (*s).to_string()).collect();
12274 let first = c.exe();
12275 let second = c.exe();
12276 assert_eq!(
12277 first, second,
12278 "Caixa::exe must be idempotent — two successive calls \
12279 on the same &self must return the same &[String]",
12280 );
12281 assert_eq!(
12282 first.as_ptr(),
12283 second.as_ptr(),
12284 "Caixa::exe must borrow the underlying Vec<String> \
12285 storage — two successive calls must return slices \
12286 with the same backing pointer (a fresh Vec<String> \
12287 clone would change the pointer on every call)",
12288 );
12289 assert_eq!(
12290 first,
12291 expected.as_slice(),
12292 "Caixa::exe must return :exe verbatim by borrow — \
12293 got {first:?}, expected {expected:?}",
12294 );
12295 }
12296 }
12297
12298 // ── Caixa::servicos — outer top-level &[T] slice accessor ─────────
12299
12300 #[test]
12301 fn servicos_returns_servicos_slice_verbatim_across_permutations() {
12302 // The canonical per-`Caixa` `:servicos` universal-axis
12303 // ComputeUnit-CR-YAML-entry-path-list slice pin:
12304 // [`Caixa::servicos`] must return the `:servicos` typed
12305 // [`Vec<String>`] list verbatim as a `&[String]`, byte-equal to
12306 // the raw `self.servicos.as_slice()` access across every
12307 // representative value in the accept-set — `[]` (the "no
12308 // ComputeUnit-CR declared" arm every `:kind` other than
12309 // `Servico` carries; the layout's [`crate::LayoutInvariants`]
12310 // `ServicoWithoutServicos` arm-gate fires exactly on this
12311 // empty-slot + `Servico`-kind combination), `[""]` (a past-the-
12312 // guard sentinel that pins the accessor doesn't perform a
12313 // silent `[""] → []` collapse on the empty-entry arm — validate
12314 // rejects `[""]` through `CodePathEmpty { slot: ":servicos" }`
12315 // but the accessor must ship the raw slot verbatim so a
12316 // validate-time gate regression surfaces at the layout /
12317 // per-Servico renderer boundary rather than being silently
12318 // absorbed into a component-drop),
12319 // `["servicos/demo.computeunit.yaml"]` (the canonical
12320 // singleton V0-shape every in-tree `caixa_with_code_paths`
12321 // positive control uses; the same shape
12322 // [`crate::require_single_servico`] admits),
12323 // `["servicos/a.computeunit.yaml", "servicos/b.computeunit.
12324 // yaml"]` (a past-the-guard `len != 1` sentinel — the V0
12325 // singularity gate rejects through `ServicoCountMismatch
12326 // { count: 2 }` but the accessor must ship the raw slot
12327 // verbatim so struct-literal `Caixa { servicos: vec![...,
12328 // ...], .. }` fixtures continue to expose the count at the
12329 // accessor), and `["servicos/a.computeunit.yaml",
12330 // "servicos/a.computeunit.yaml"]` (a past-the-guard duplicate
12331 // sentinel — validate rejects through
12332 // `CodePathDuplicate { slot: ":servicos" }` per the per-slot
12333 // set-not-multiset gate, but the accessor must ship the raw
12334 // slot verbatim so struct-literal fixtures continue to expose
12335 // the duplicate at the accessor).
12336 //
12337 // Fifth and final outer top-level [`Caixa`] `&[T]`-return
12338 // slice accessor pin on the substrate primitive — folds on the
12339 // "outer [`Caixa`] `&[T]` slice" projection pattern
12340 // `autores_returns_autores_slice_verbatim_across_permutations`
12341 // (b5d813f) opened,
12342 // `etiquetas_returns_etiquetas_slice_verbatim_across_permutations`
12343 // (78c7d3c) folded on,
12344 // `bibliotecas_returns_bibliotecas_slice_verbatim_across_permutations`
12345 // (8a36c23) closed the universal-axis text-tag family of, and
12346 // `exe_returns_exe_slice_verbatim_across_permutations`
12347 // (65d9527) opened the foreign-code-slot sub-family of. Closes
12348 // the outer-`Caixa` foreign-code-slot `&[T]` sub-family — the
12349 // trio of code-surface list slots (`:bibliotecas` + `:exe` +
12350 // `:servicos`) now each carries a substrate-canonical slice
12351 // accessor. Pins against a future silent detour that returned
12352 // an owned `Vec<String>` (which would type-check but silently
12353 // clone on every accessor call, breaking the zero-cost
12354 // projection every peer sibling slice accessor carries), a
12355 // `[""] → []` collapse (which would silently absorb the
12356 // `CodePathEmpty` refusal case at the accessor boundary), an
12357 // `[a, a] → [a]` dedup collapse (which would silently absorb
12358 // the `CodePathDuplicate` refusal case at the accessor
12359 // boundary — the per-slot set-not-multiset gate is downstream
12360 // of the accessor and must not be silently promoted into it),
12361 // or a `[a, b] → [a]` singleton collapse (which would silently
12362 // absorb the V0 `ServicoCountMismatch` refusal case at the
12363 // accessor boundary — the V0 singularity gate is downstream of
12364 // the accessor and must not be silently promoted into it).
12365 for servicos in [
12366 vec![],
12367 vec![""],
12368 vec!["servicos/demo.computeunit.yaml"],
12369 vec!["servicos/a.computeunit.yaml", "servicos/b.computeunit.yaml"],
12370 vec!["servicos/a.computeunit.yaml", "servicos/a.computeunit.yaml"],
12371 ] {
12372 let c = caixa_with_code_paths(vec![], vec![], servicos.clone());
12373 let expected: Vec<String> = servicos.iter().map(|s| (*s).to_string()).collect();
12374 assert_eq!(
12375 c.servicos(),
12376 expected.as_slice(),
12377 "Caixa::servicos must return :servicos verbatim (got \
12378 {:?}, expected {expected:?})",
12379 c.servicos(),
12380 );
12381 assert_eq!(
12382 c.servicos(),
12383 c.servicos.as_slice(),
12384 "Caixa::servicos must byte-equal the raw \
12385 `self.servicos.as_slice()` field access across every \
12386 value in the Vec<String> accept-set",
12387 );
12388 }
12389 }
12390
12391 #[test]
12392 fn validate_code_paths_servicos_empty_arm_routes_through_accessor() {
12393 // Composition pin: [`Caixa::validate_code_paths`]'s per-entry
12394 // empty-arm gate on the `:servicos` slot must key off
12395 // [`Caixa::servicos`], not a divergent raw `&self.servicos`
12396 // field-borrow walk. Structurally: a `Caixa { servicos:
12397 // vec!["".into()], .. }` must surface the `CodePathEmpty
12398 // { slot: ":servicos" }` refusal exactly, and a `Caixa
12399 // { servicos: vec!["servicos/demo.computeunit.yaml".into()],
12400 // .. }` (the canonical singleton V0-shape every in-tree
12401 // `caixa_with_code_paths` positive control uses) must pass
12402 // validate. The pair jointly pins the accessor + validate-gate
12403 // composition: any future silent detour that had the accessor
12404 // return an empty slice on the `[""]` arm (a `.iter().filter
12405 // (|s| !s.is_empty()).collect()` collapse) would silently
12406 // absorb the `CodePathEmpty` refusal at the accessor boundary
12407 // and the validate gate would accept a struct-literal
12408 // `Caixa { servicos: vec!["".into()], .. }` — the composition
12409 // pin catches that at caixa-core build time.
12410 //
12411 // Peer of the per-`Caixa`
12412 // `validate_code_paths_bibliotecas_empty_arm_routes_through_accessor`
12413 // (8a36c23), `validate_code_paths_exe_empty_arm_routes_through_accessor`
12414 // (65d9527), `validate_autores_empty_arm_routes_through_accessor`
12415 // (b5d813f), and
12416 // `validate_etiquetas_empty_entry_arm_routes_through_accessor`
12417 // (78c7d3c) accessor-composition pins on the sibling `&[T]`-
12418 // composition axes — same "the validate / shape-gate predicate
12419 // must route through the substrate-primitive typed dispatch"
12420 // discipline extended onto the sibling outer top-level
12421 // [`Caixa`] `&[T]`-composition surface, closing the trio of
12422 // code-surface accessor-composition pins on the same axis.
12423 // Nominally the in-tree `validate_code_paths` production body
12424 // still keys off the internal
12425 // `[(":bibliotecas", &self.bibliotecas,
12426 // CodePathFileType::LispSource), (":exe", &self.exe, ..),
12427 // (":servicos", &self.servicos, ..)]` per-slot dispatch tuple
12428 // (the tuple's homogeneous `&Vec<String>`-typed shape blocks a
12429 // per-element accessor swap in isolation — a future companion
12430 // lift promotes the tuple's element type to `&[String]` and
12431 // threads the triple of typed dispatches through as a unit);
12432 // the composition pin catches any future accessor-side silent
12433 // filter drop against that eventual tuple-closure regardless
12434 // of whether the `:servicos` slot is threaded through the
12435 // accessor or the raw field access at the tuple's construction
12436 // site.
12437 let c = caixa_with_code_paths(vec![], vec![], vec![""]);
12438 assert!(
12439 matches!(
12440 c.validate_code_paths(),
12441 Err(ManifestError::CodePathEmpty { slot: ":servicos" })
12442 ),
12443 "validate_code_paths must reject servicos == vec![\"\"] \
12444 with CodePathEmpty {{ slot: \":servicos\" }} — the \
12445 accessor and the validate gate must route through the \
12446 same substrate-primitive typed dispatch on the \
12447 :servicos per-entry empty arm",
12448 );
12449 let c = caixa_with_code_paths(vec![], vec![], vec!["servicos/demo.computeunit.yaml"]);
12450 assert!(
12451 c.validate_code_paths().is_ok(),
12452 "validate_code_paths must accept servicos == \
12453 vec![\"servicos/demo.computeunit.yaml\"] (the canonical \
12454 singleton V0-shape every in-tree `caixa_with_code_paths` \
12455 positive control uses)",
12456 );
12457 }
12458
12459 #[test]
12460 fn servicos_projects_slice_by_borrow() {
12461 // The by-borrow pin: [`Caixa::servicos`] returns `&[String]` by
12462 // borrow — the returned slice borrows the underlying
12463 // `Vec<String>` storage of the `:servicos` slot and the
12464 // accessor must not clone the backing `Vec` on every call.
12465 // Peer of the per-`Caixa` `autores_projects_slice_by_borrow`
12466 // (b5d813f), `etiquetas_projects_slice_by_borrow` (78c7d3c),
12467 // `bibliotecas_projects_slice_by_borrow` (8a36c23), and
12468 // `exe_projects_slice_by_borrow` (65d9527) by-borrow pins on
12469 // the sibling outer top-level [`Caixa`] `&[String]`-return
12470 // axes — the accessor's returned slice must borrow from
12471 // `&self` (the returned reference's lifetime is tied to
12472 // `&self`), and calling the accessor twice on the same
12473 // [`Caixa`] must yield slices that are pointer-equal (the
12474 // underlying byte-buffer is the storage `Vec`'s allocation,
12475 // not a fresh copy) as well as value-equal (idempotent, no
12476 // side effects on `&self`).
12477 //
12478 // Pins against a future silent detour that returned an owned
12479 // `Vec<String>` (which would type-check but silently clone on
12480 // every call, breaking the zero-cost projection every peer
12481 // sibling slice accessor carries), a `&Vec<String>` return
12482 // (which would leak the backing `Vec`'s grow/push/reserve
12483 // surface no downstream consumer reaches for), or a one-arm-
12484 // only accessor that returned a saturating value on some
12485 // sentinel input (breaking the pass-through invariant the
12486 // sibling slice accessors carry).
12487 for servicos in [
12488 vec![],
12489 vec!["servicos/demo.computeunit.yaml"],
12490 vec!["servicos/a.computeunit.yaml", "servicos/b.computeunit.yaml"],
12491 vec!["servicos/a.computeunit.yaml", "servicos/a.computeunit.yaml"],
12492 ] {
12493 let c = caixa_with_code_paths(vec![], vec![], servicos.clone());
12494 let expected: Vec<String> = servicos.iter().map(|s| (*s).to_string()).collect();
12495 let first = c.servicos();
12496 let second = c.servicos();
12497 assert_eq!(
12498 first, second,
12499 "Caixa::servicos must be idempotent — two successive \
12500 calls on the same &self must return the same &[String]",
12501 );
12502 assert_eq!(
12503 first.as_ptr(),
12504 second.as_ptr(),
12505 "Caixa::servicos must borrow the underlying \
12506 Vec<String> storage — two successive calls must \
12507 return slices with the same backing pointer (a fresh \
12508 Vec<String> clone would change the pointer on every \
12509 call)",
12510 );
12511 assert_eq!(
12512 first,
12513 expected.as_slice(),
12514 "Caixa::servicos must return :servicos verbatim by \
12515 borrow — got {first:?}, expected {expected:?}",
12516 );
12517 }
12518 }
12519
12520 // ── Caixa::deps — outer top-level &[Dep] slice accessor ───────────
12521
12522 fn caixa_with_deps(deps: Vec<Dep>) -> Caixa {
12523 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
12524 c.deps = deps;
12525 c
12526 }
12527
12528 #[test]
12529 fn deps_returns_deps_slice_verbatim_across_permutations() {
12530 // The canonical per-`Caixa` `:deps` universal-axis runtime-
12531 // dependency-declaration-list slice pin: [`Caixa::deps`] must
12532 // return the `:deps` typed [`Vec<Dep>`] list verbatim as a
12533 // `&[Dep]`, element-equal to the raw `self.deps.as_slice()`
12534 // access across every representative value in the accept-set —
12535 // `[]` (the "no runtime deps declared" arm every existing
12536 // fixture without a `:deps` line carries; the
12537 // [`Caixa::template`] scaffold emits `:deps ()`), a canonical
12538 // single-entry list (the shape most consumer caixas carry), a
12539 // canonical two-entry list (the multi-dep runtime closure), and
12540 // two past-the-guard sentinels — a `[""]`-`:nome` entry
12541 // ([`Self::validate_deps`] rejects through `NomeEmpty` /
12542 // `NomeInvalid` but the accessor must ship the raw slot
12543 // verbatim) and a `[a, a]` duplicate (validate rejects through
12544 // `DuplicateNome { list: ":deps" }` but the accessor must ship
12545 // the raw slot verbatim so struct-literal fixtures continue to
12546 // expose the duplicate at the accessor).
12547 //
12548 // First outer top-level [`Caixa`] `&[Dep]`-return slice accessor
12549 // pin on the substrate primitive — opens the outer-`Caixa`
12550 // dependency-slot `&[Dep]` sub-family the sibling `:deps-dev`
12551 // future lift closes on. Peer of the closed outer-`Caixa`
12552 // foreign-code-slot `&[String]` sub-family
12553 // (`bibliotecas_returns_bibliotecas_slice_verbatim_across_permutations`
12554 // 8a36c23, `exe_returns_exe_slice_verbatim_across_permutations`
12555 // 65d9527, `servicos_returns_servicos_slice_verbatim_across_permutations`
12556 // 611f78b) and the outer-`Caixa` universal-axis text-tag family
12557 // (`autores_returns_autores_slice_verbatim_across_permutations`
12558 // b5d813f, `etiquetas_returns_etiquetas_slice_verbatim_across_permutations`
12559 // 78c7d3c) — extends the "outer [`Caixa`] `&[T]` slice"
12560 // projection pattern onto a novel element-type axis (`Dep`
12561 // composite vs the prior sibling family's `String` scalar).
12562 // Pins against a future silent detour that returned an owned
12563 // `Vec<Dep>` (which would type-check but silently clone on every
12564 // accessor call, breaking the zero-cost projection every peer
12565 // sibling slice accessor carries), a `[""] → []` collapse (which
12566 // would silently absorb the `NomeEmpty` refusal case at the
12567 // accessor boundary), or a `[a, a] → [a]` dedup collapse (which
12568 // would silently absorb the `DuplicateNome` refusal case at the
12569 // accessor boundary).
12570 for deps in [
12571 vec![],
12572 vec![Dep::simple("", "^0.1")],
12573 vec![Dep::simple("caixa-teia", "^0.1")],
12574 vec![
12575 Dep::simple("caixa-teia", "^0.1"),
12576 Dep::simple("caixa-core", "^0.1"),
12577 ],
12578 vec![
12579 Dep::simple("caixa-teia", "^0.1"),
12580 Dep::simple("caixa-teia", "^0.2"),
12581 ],
12582 ] {
12583 let c = caixa_with_deps(deps.clone());
12584 assert_eq!(
12585 c.deps(),
12586 deps.as_slice(),
12587 "Caixa::deps must return :deps verbatim (got {:?}, \
12588 expected {deps:?})",
12589 c.deps(),
12590 );
12591 assert_eq!(
12592 c.deps(),
12593 c.deps.as_slice(),
12594 "Caixa::deps must element-equal the raw \
12595 `self.deps.as_slice()` field access across every \
12596 value in the Vec<Dep> accept-set",
12597 );
12598 }
12599 }
12600
12601 #[test]
12602 fn validate_deps_duplicate_arm_routes_through_accessor() {
12603 // Composition pin: [`Caixa::validate_deps`]'s within-`:deps`
12604 // duplicate-`:nome` gate must key off [`Caixa::deps`], not the
12605 // raw `&self.deps` field-borrow walk. Structurally: a `Caixa
12606 // { deps: vec![Dep::simple("d", "^0.1"), Dep::simple("d",
12607 // "^0.2")], .. }` must surface the `DuplicateNome { list:
12608 // ":deps" }` refusal exactly, and a `Caixa { deps: vec![
12609 // Dep::simple("d", "^0.1")], .. }` (the canonical single-entry
12610 // form) must pass validate. The pair jointly pins the accessor +
12611 // validate-gate composition: any future silent detour that had
12612 // the accessor return a dedupped slice on the `[a, a]` arm (a
12613 // `.iter().unique_by(|d| d.nome.as_str()).collect()` collapse)
12614 // would silently absorb the `DuplicateNome` refusal at the
12615 // accessor boundary and the validate gate would accept a
12616 // struct-literal `Caixa` carrying the drift — the composition
12617 // pin catches that at caixa-core build time.
12618 //
12619 // Peer of the per-`Caixa`
12620 // `validate_autores_empty_entry_arm_routes_through_accessor`
12621 // (b5d813f), `validate_etiquetas_empty_entry_arm_routes_through_accessor`
12622 // (78c7d3c), `validate_code_paths_bibliotecas_empty_arm_routes_through_accessor`
12623 // (8a36c23), `validate_code_paths_exe_empty_arm_routes_through_accessor`
12624 // (65d9527), and `validate_code_paths_servicos_empty_arm_routes_through_accessor`
12625 // (611f78b) accessor-composition pins on the sibling `&[T]`-
12626 // composition axes — same "the validate gate must route through
12627 // the substrate-primitive typed dispatch" discipline extended
12628 // onto the sibling outer top-level [`Caixa`] `&[Dep]`-
12629 // composition surface, opening the outer-`Caixa` dependency-slot
12630 // arm of the composition-pin family.
12631 let c = caixa_with_deps(vec![Dep::simple("d", "^0.1"), Dep::simple("d", "^0.2")]);
12632 let err = c.validate_deps().unwrap_err();
12633 assert!(
12634 matches!(
12635 err,
12636 DepError::DuplicateNome { ref nome, list } if nome == "d"
12637 && list == crate::render::DEP_AUTHOR_KEY_DEPS
12638 ),
12639 "validate_deps must reject deps == \
12640 vec![Dep(\"d\",\"^0.1\"), Dep(\"d\",\"^0.2\")] with \
12641 DuplicateNome {{ nome: \"d\", list: \":deps\" }} — the \
12642 accessor and the validate gate must route through the \
12643 same substrate-primitive typed dispatch on the :deps \
12644 within-list duplicate arm (got {err:?})",
12645 );
12646 let c = caixa_with_deps(vec![Dep::simple("d", "^0.1")]);
12647 assert!(
12648 c.validate_deps().is_ok(),
12649 "validate_deps must accept deps == vec![Dep(\"d\",\"^0.1\")] \
12650 (the canonical single-entry form)",
12651 );
12652 }
12653
12654 #[test]
12655 fn deps_projects_slice_by_borrow() {
12656 // The by-borrow pin: [`Caixa::deps`] returns `&[Dep]` by borrow
12657 // — the returned slice borrows the underlying `Vec<Dep>` storage
12658 // of the `:deps` slot and the accessor must not clone the
12659 // backing `Vec` on every call. Peer of the per-`Caixa`
12660 // `autores_projects_slice_by_borrow` (b5d813f),
12661 // `etiquetas_projects_slice_by_borrow` (78c7d3c),
12662 // `bibliotecas_projects_slice_by_borrow` (8a36c23),
12663 // `exe_projects_slice_by_borrow` (65d9527), and
12664 // `servicos_projects_slice_by_borrow` (611f78b) by-borrow pins
12665 // on the sibling outer top-level [`Caixa`] `&[String]`-return
12666 // axes — the accessor's returned slice must borrow from `&self`
12667 // (the returned reference's lifetime is tied to `&self`), and
12668 // calling the accessor twice on the same [`Caixa`] must yield
12669 // slices that are pointer-equal (the underlying byte-buffer is
12670 // the storage `Vec`'s allocation, not a fresh copy) as well as
12671 // value-equal (idempotent, no side effects on `&self`).
12672 //
12673 // Pins against a future silent detour that returned an owned
12674 // `Vec<Dep>` (which would type-check but silently clone on
12675 // every call), a `&Vec<Dep>` return (which would leak the
12676 // backing `Vec`'s grow/push/reserve surface no downstream
12677 // consumer reaches for), or a one-arm-only accessor that
12678 // returned a saturating value on some sentinel input.
12679 for deps in [
12680 vec![],
12681 vec![Dep::simple("caixa-teia", "^0.1")],
12682 vec![
12683 Dep::simple("caixa-teia", "^0.1"),
12684 Dep::simple("caixa-core", "^0.1"),
12685 ],
12686 ] {
12687 let c = caixa_with_deps(deps.clone());
12688 let first = c.deps();
12689 let second = c.deps();
12690 assert_eq!(
12691 first, second,
12692 "Caixa::deps must be idempotent — two successive calls \
12693 on the same &self must return the same &[Dep]",
12694 );
12695 assert_eq!(
12696 first.as_ptr(),
12697 second.as_ptr(),
12698 "Caixa::deps must borrow the underlying Vec<Dep> \
12699 storage — two successive calls must return slices \
12700 with the same backing pointer (a fresh Vec<Dep> clone \
12701 would change the pointer on every call)",
12702 );
12703 assert_eq!(
12704 first,
12705 deps.as_slice(),
12706 "Caixa::deps must return :deps verbatim by borrow — \
12707 got {first:?}, expected {deps:?}",
12708 );
12709 }
12710 }
12711
12712 // ── Caixa::deps_dev — outer top-level &[Dep] slice accessor ──────
12713
12714 fn caixa_with_deps_dev(deps_dev: Vec<Dep>) -> Caixa {
12715 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
12716 c.deps_dev = deps_dev;
12717 c
12718 }
12719
12720 #[test]
12721 fn deps_dev_returns_deps_dev_slice_verbatim_across_permutations() {
12722 // The canonical per-`Caixa` `:deps-dev` universal-axis dev-only-
12723 // dependency-declaration-list slice pin: [`Caixa::deps_dev`]
12724 // must return the `:deps-dev` typed [`Vec<Dep>`] list verbatim as
12725 // a `&[Dep]`, element-equal to the raw `self.deps_dev.as_slice()`
12726 // access across every representative value in the accept-set —
12727 // `[]` (the "no dev deps declared" arm every existing fixture
12728 // without a `:deps-dev` line carries; the [`Caixa::template`]
12729 // scaffold emits `:deps-dev ()`), a canonical single-entry list
12730 // (the shape most consumer caixas carry — a `tatara-check` dev
12731 // pin), a canonical two-entry list (the multi-dev-dep closure),
12732 // and two past-the-guard sentinels — a `[""]`-`:nome` entry
12733 // ([`Self::validate_deps`] rejects through `NomeEmpty` /
12734 // `NomeInvalid` but the accessor must ship the raw slot
12735 // verbatim) and a `[a, a]` duplicate (validate rejects through
12736 // `DuplicateNome { list: ":deps-dev" }` but the accessor must
12737 // ship the raw slot verbatim so struct-literal fixtures continue
12738 // to expose the duplicate at the accessor).
12739 //
12740 // Second outer top-level [`Caixa`] `&[Dep]`-return slice-accessor
12741 // pin on the substrate primitive — closes the outer-`Caixa`
12742 // dependency-slot `&[Dep]` sub-family the sibling
12743 // `deps_returns_deps_slice_verbatim_across_permutations`
12744 // (ad34b4e) opened on. Folds the "outer [`Caixa`] `&[Dep]`
12745 // slice" projection pattern onto the sibling dev-dep axis —
12746 // pins against a future silent detour that returned an owned
12747 // `Vec<Dep>` (which would type-check but silently clone on every
12748 // accessor call, breaking the zero-cost projection every peer
12749 // sibling slice accessor carries), a `[""] → []` collapse (which
12750 // would silently absorb the `NomeEmpty` refusal case at the
12751 // accessor boundary), or a `[a, a] → [a]` dedup collapse (which
12752 // would silently absorb the `DuplicateNome` refusal case at the
12753 // accessor boundary).
12754 for deps_dev in [
12755 vec![],
12756 vec![Dep::simple("", "^0.1")],
12757 vec![Dep::simple("tatara-check", "^0.1")],
12758 vec![
12759 Dep::simple("tatara-check", "^0.1"),
12760 Dep::simple("caixa-lint", "^0.1"),
12761 ],
12762 vec![
12763 Dep::simple("tatara-check", "^0.1"),
12764 Dep::simple("tatara-check", "^0.2"),
12765 ],
12766 ] {
12767 let c = caixa_with_deps_dev(deps_dev.clone());
12768 assert_eq!(
12769 c.deps_dev(),
12770 deps_dev.as_slice(),
12771 "Caixa::deps_dev must return :deps-dev verbatim (got \
12772 {:?}, expected {deps_dev:?})",
12773 c.deps_dev(),
12774 );
12775 assert_eq!(
12776 c.deps_dev(),
12777 c.deps_dev.as_slice(),
12778 "Caixa::deps_dev must element-equal the raw \
12779 `self.deps_dev.as_slice()` field access across every \
12780 value in the Vec<Dep> accept-set",
12781 );
12782 }
12783 }
12784
12785 #[test]
12786 fn validate_deps_duplicate_deps_dev_arm_routes_through_accessor() {
12787 // Composition pin: [`Caixa::validate_deps`]'s within-`:deps-dev`
12788 // duplicate-`:nome` gate must key off [`Caixa::deps_dev`], not
12789 // the raw `&self.deps_dev` field-borrow walk. Structurally: a
12790 // `Caixa { deps_dev: vec![Dep::simple("d", "^0.1"),
12791 // Dep::simple("d", "^0.2")], .. }` must surface the
12792 // `DuplicateNome { list: ":deps-dev" }` refusal exactly, and a
12793 // `Caixa { deps_dev: vec![Dep::simple("d", "^0.1")], .. }` (the
12794 // canonical single-entry form) must pass validate. The pair
12795 // jointly pins the accessor + validate-gate composition: any
12796 // future silent detour that had the accessor return a dedupped
12797 // slice on the `[a, a]` arm (a
12798 // `.iter().unique_by(|d| d.nome.as_str()).collect()` collapse)
12799 // would silently absorb the `DuplicateNome` refusal at the
12800 // accessor boundary and the validate gate would accept a
12801 // struct-literal `Caixa` carrying the drift — the composition
12802 // pin catches that at caixa-core build time.
12803 //
12804 // Peer of `validate_deps_duplicate_arm_routes_through_accessor`
12805 // (ad34b4e) on the sibling `:deps` axis — same "the validate
12806 // gate must route through the substrate-primitive typed
12807 // dispatch" discipline folded onto the sibling `:deps-dev`
12808 // axis, closing the two-list dep-graph composition-pin family.
12809 // The `:deps-dev` diagnostic must carry the
12810 // `DEP_AUTHOR_KEY_DEPS_DEV` list-tag (not
12811 // `DEP_AUTHOR_KEY_DEPS`) so the emitted error names the
12812 // offending list unambiguously.
12813 let c = caixa_with_deps_dev(vec![Dep::simple("d", "^0.1"), Dep::simple("d", "^0.2")]);
12814 let err = c.validate_deps().unwrap_err();
12815 assert!(
12816 matches!(
12817 err,
12818 DepError::DuplicateNome { ref nome, list } if nome == "d"
12819 && list == crate::render::DEP_AUTHOR_KEY_DEPS_DEV
12820 ),
12821 "validate_deps must reject deps_dev == \
12822 vec![Dep(\"d\",\"^0.1\"), Dep(\"d\",\"^0.2\")] with \
12823 DuplicateNome {{ nome: \"d\", list: \":deps-dev\" }} — the \
12824 accessor and the validate gate must route through the \
12825 same substrate-primitive typed dispatch on the :deps-dev \
12826 within-list duplicate arm (got {err:?})",
12827 );
12828 let c = caixa_with_deps_dev(vec![Dep::simple("d", "^0.1")]);
12829 assert!(
12830 c.validate_deps().is_ok(),
12831 "validate_deps must accept deps_dev == \
12832 vec![Dep(\"d\",\"^0.1\")] (the canonical single-entry form)",
12833 );
12834 }
12835
12836 #[test]
12837 fn deps_dev_projects_slice_by_borrow() {
12838 // The by-borrow pin: [`Caixa::deps_dev`] returns `&[Dep]` by
12839 // borrow — the returned slice borrows the underlying `Vec<Dep>`
12840 // storage of the `:deps-dev` slot and the accessor must not
12841 // clone the backing `Vec` on every call. Peer of
12842 // `deps_projects_slice_by_borrow` (ad34b4e) on the sibling
12843 // `:deps` axis, and of the per-`Caixa`
12844 // `autores_projects_slice_by_borrow` (b5d813f),
12845 // `etiquetas_projects_slice_by_borrow` (78c7d3c),
12846 // `bibliotecas_projects_slice_by_borrow` (8a36c23),
12847 // `exe_projects_slice_by_borrow` (65d9527), and
12848 // `servicos_projects_slice_by_borrow` (611f78b) by-borrow pins
12849 // on the sibling outer top-level [`Caixa`] `&[String]`-return
12850 // axes — the accessor's returned slice must borrow from `&self`
12851 // (the returned reference's lifetime is tied to `&self`), and
12852 // calling the accessor twice on the same [`Caixa`] must yield
12853 // slices that are pointer-equal (the underlying byte-buffer is
12854 // the storage `Vec`'s allocation, not a fresh copy) as well as
12855 // value-equal (idempotent, no side effects on `&self`).
12856 //
12857 // Pins against a future silent detour that returned an owned
12858 // `Vec<Dep>` (which would type-check but silently clone on
12859 // every call), a `&Vec<Dep>` return (which would leak the
12860 // backing `Vec`'s grow/push/reserve surface no downstream
12861 // consumer reaches for), or a one-arm-only accessor that
12862 // returned a saturating value on some sentinel input.
12863 for deps_dev in [
12864 vec![],
12865 vec![Dep::simple("tatara-check", "^0.1")],
12866 vec![
12867 Dep::simple("tatara-check", "^0.1"),
12868 Dep::simple("caixa-lint", "^0.1"),
12869 ],
12870 ] {
12871 let c = caixa_with_deps_dev(deps_dev.clone());
12872 let first = c.deps_dev();
12873 let second = c.deps_dev();
12874 assert_eq!(
12875 first, second,
12876 "Caixa::deps_dev must be idempotent — two successive \
12877 calls on the same &self must return the same &[Dep]",
12878 );
12879 assert_eq!(
12880 first.as_ptr(),
12881 second.as_ptr(),
12882 "Caixa::deps_dev must borrow the underlying Vec<Dep> \
12883 storage — two successive calls must return slices \
12884 with the same backing pointer (a fresh Vec<Dep> clone \
12885 would change the pointer on every call)",
12886 );
12887 assert_eq!(
12888 first,
12889 deps_dev.as_slice(),
12890 "Caixa::deps_dev must return :deps-dev verbatim by \
12891 borrow — got {first:?}, expected {deps_dev:?}",
12892 );
12893 }
12894 }
12895
12896 // ── Caixa::limits — outer top-level Option<&LimitsSpec> composite-reference accessor ──
12897
12898 fn caixa_with_limits(limits: Option<crate::LimitsSpec>) -> Caixa {
12899 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
12900 c.limits = limits;
12901 c
12902 }
12903
12904 #[test]
12905 fn limits_returns_limits_option_ref_verbatim_across_permutations() {
12906 // The canonical per-`Caixa` `:limits` M2 typed-slot outer-
12907 // composite optional-composite-reference-shape pin:
12908 // [`Caixa::limits`] must return the `:limits` typed
12909 // `Option<LimitsSpec>` verbatim as an `Option<&LimitsSpec>`
12910 // reference over the same backing storage the raw
12911 // `self.limits.as_ref()` field access borrows from, byte-equal
12912 // across every representative fixture in the accept-set — the
12913 // author-omitted `None` shape (the "engine-default applies"
12914 // partition every downstream Servico M2 overlay emitter treats
12915 // as "emit nothing"), the empty-composite `Some(LimitsSpec {
12916 // .. default })` shape ([`LimitsSpec::is_empty`] holds — every
12917 // per-axis cap is `None`, so the peer M2 overlay emitter's
12918 // `.is_empty()`-gated projection still emits nothing but the
12919 // outer presence-bit is `Some`, so [`Caixa::declared_servico_slots`]
12920 // still pushes the `M2_AUTHOR_KEY_LIMITS` label), a single-axis
12921 // fixture (only `:memory` set — the canonical shape most
12922 // memory-heavy Servicos carry), and a fully-populated composite
12923 // (every per-axis cap set — the canonical shape a
12924 // sandboxed-by-default Servico carries).
12925 //
12926 // Pins against a future silent detour that returned a fresh-
12927 // cloned [`LimitsSpec`] copy (which would type-check via the
12928 // `Clone` impl but silently break every downstream caller that
12929 // relied on the reference sharing the composite's backing
12930 // identity), a reference to an operator-resolved overlay (the
12931 // future per-cluster `:limits-overrides` slot — its resolution
12932 // must land at exactly this accessor body, not silently divert
12933 // the raw slot away from a second consumer), a
12934 // `None` → `Some(LimitsSpec::default)` cluster-default
12935 // projection (which would collapse the load-bearing
12936 // "author-omitted `:limits` ⇒ engine-default applies" partition
12937 // the peer [`crate::render::servico_m2_overlay`] emitter and
12938 // the peer [`Caixa::declared_servico_slots`] enumerator both
12939 // read), or an axis-shuffled projection (a future detour that
12940 // swapped `memory` and `fuel` through the accessor would
12941 // silently split the paired [`crate::StandardLayout::verify`]
12942 // per-`:limits` shape gate's traversal input from the peer
12943 // `servico_m2_overlay` emitter's projection input).
12944 //
12945 // First outer top-level [`Caixa`] `Option<&Composite>`-return
12946 // composite-reference accessor pin on the substrate primitive
12947 // — opens the outer-`Caixa` `Option<&Composite>` composite-
12948 // reference projection pattern the sibling `:behavior`
12949 // [`crate::BehaviorSpec`] / `:politicas`
12950 // [`crate::aplicacao::MeshPolicy`] / `:placement`
12951 // [`crate::aplicacao::Placement`] / `:entrada`
12952 // [`crate::aplicacao::Entrada`] future outer-composite lifts
12953 // fold on. Peer of the closed M3 outer-composite family the
12954 // sibling [`crate::AplicacaoSpec::politicas`] (534dc21) /
12955 // [`crate::AplicacaoSpec::placement`] (9abb8f0) /
12956 // [`crate::AplicacaoSpec::entrada`] (d32111c) composite-
12957 // reference accessor pins already carry on the outer
12958 // [`crate::AplicacaoSpec`] altitude — extends the outer-
12959 // accessor byte-equal-projection discipline onto the outer
12960 // top-level [`Caixa`] M2 Servico-runtime slot altitude.
12961 use crate::LimitsSpec;
12962 use std::time::Duration;
12963 let fixtures: Vec<Option<LimitsSpec>> = vec![
12964 None,
12965 Some(LimitsSpec::default()),
12966 Some(LimitsSpec {
12967 memory: Some(64 * 1024 * 1024),
12968 ..Default::default()
12969 }),
12970 Some(LimitsSpec {
12971 memory: Some(64 * 1024 * 1024),
12972 fuel: Some(1_000_000),
12973 wall_clock: Some(Duration::from_secs(30)),
12974 cpu: Some(500),
12975 }),
12976 ];
12977 for limits in fixtures {
12978 let c = caixa_with_limits(limits.clone());
12979 assert_eq!(
12980 c.limits(),
12981 limits.as_ref(),
12982 "Caixa::limits must return :limits verbatim (got {:?}, \
12983 expected {:?})",
12984 c.limits(),
12985 limits.as_ref(),
12986 );
12987 match (c.limits(), c.limits.as_ref()) {
12988 (Some(a), Some(b)) => assert!(
12989 std::ptr::eq(a, b),
12990 "Caixa::limits accessor and self.limits.as_ref() \
12991 field access must borrow the same backing storage \
12992 — the accessor is the substrate-primitive typed \
12993 dispatch every downstream Servico-M2-overlay \
12994 composite consumer must route through, and a \
12995 reference-identity split would silently break \
12996 every consumer that relied on the borrow sharing \
12997 the composite's storage",
12998 ),
12999 (None, None) => {}
13000 _ => panic!(
13001 "Caixa::limits presence bit must byte-equal \
13002 self.limits.is_some() — a presence-bit drift would \
13003 silently split the paired StandardLayout::verify \
13004 per-`:limits` shape gate's traversal head from \
13005 the peer render::servico_m2_overlay M2 overlay \
13006 emitter's traversal head from the peer \
13007 Caixa::declared_servico_slots M2 declared-slot \
13008 enumerator's presence probe",
13009 ),
13010 }
13011 assert_eq!(
13012 c.limits().is_some(),
13013 c.limits.is_some(),
13014 "Caixa::limits().is_some() must byte-equal \
13015 self.limits.is_some() — a presence-bit drift would \
13016 silently split every downstream Option<&LimitsSpec> \
13017 consumer's partition on the engine-default arm",
13018 );
13019 }
13020 }
13021
13022 #[test]
13023 fn declared_servico_slots_limits_arm_routes_through_accessor() {
13024 // Composition pin: [`Caixa::declared_servico_slots`]'s
13025 // `:limits` presence-probe arm must key off [`Caixa::limits`],
13026 // not the raw `self.limits.is_some()` field-probe. Structurally:
13027 // a `Caixa { limits: Some(LimitsSpec::default()), .. }` must
13028 // still push `M2_AUTHOR_KEY_LIMITS` onto the declared-slot list
13029 // (the presence bit is `Some`, so the M2 kind-coherence gate
13030 // must surface the slot as "declared" even when every per-axis
13031 // cap is unset), and a `Caixa { limits: None, .. }` must NOT
13032 // push the label (the "author omitted the slot entirely"
13033 // partition). The pair jointly pins the accessor + declared-
13034 // slot enumerator composition: any future silent detour that
13035 // had the accessor collapse `Some(LimitsSpec::default())` to
13036 // `None` (a `.filter(|l| !l.is_empty())` projection) would
13037 // silently absorb the "declared but empty" arm at the
13038 // accessor boundary and the [`crate::LayoutError::ServicoSlotsOnNonServico`]
13039 // kind-coherence gate would silently accept a
13040 // struct-literal `Caixa` carrying the drift.
13041 //
13042 // Peer of the sibling per-`Caixa`
13043 // `validate_deps_duplicate_arm_routes_through_accessor` (ad34b4e)
13044 // and `validate_deps_duplicate_deps_dev_arm_routes_through_accessor`
13045 // (f7fd81e) accessor-composition pins on the sibling `:deps` /
13046 // `:deps-dev` outer-`&[Dep]`-composition axes — same "the
13047 // enumerator gate must route through the substrate-primitive
13048 // typed dispatch" discipline extended onto the outer top-level
13049 // [`Caixa`] `Option<&LimitsSpec>`-composition surface, opening
13050 // the outer-`Caixa` M2 Servico-runtime-slot arm of the
13051 // composition-pin family.
13052 use crate::LimitsSpec;
13053 let c = caixa_with_limits(Some(LimitsSpec::default()));
13054 let slots = c.declared_servico_slots();
13055 assert!(
13056 slots.contains(&crate::render::M2_AUTHOR_KEY_LIMITS),
13057 "declared_servico_slots must push M2_AUTHOR_KEY_LIMITS \
13058 when `:limits` is Some (even for LimitsSpec::default()) \
13059 — the accessor and the enumerator gate must route through \
13060 the same substrate-primitive typed dispatch on the outer \
13061 :limits presence bit (got slots={slots:?})",
13062 );
13063 let c = caixa_with_limits(None);
13064 let slots = c.declared_servico_slots();
13065 assert!(
13066 !slots.contains(&crate::render::M2_AUTHOR_KEY_LIMITS),
13067 "declared_servico_slots must NOT push M2_AUTHOR_KEY_LIMITS \
13068 when `:limits` is None — the author-omitted arm must \
13069 route through the accessor's None-return unchanged (got \
13070 slots={slots:?})",
13071 );
13072 }
13073
13074 #[test]
13075 fn servico_m2_overlay_limits_arm_routes_through_accessor() {
13076 // Composition pin: [`crate::render::servico_m2_overlay`]'s
13077 // per-`:limits` M2 overlay emit arm must key off
13078 // [`Caixa::limits`], not the raw `&caixa.limits` field-borrow.
13079 // Structurally: a `Caixa { limits: Some(LimitsSpec { memory:
13080 // Some(64 MiB), .. default }), .. }` must surface the
13081 // `M2_KEY_LIMITS` key with the per-axis
13082 // `memory: "64MiB"` sub-mapping in the overlay, a `Caixa {
13083 // limits: Some(LimitsSpec::default()), .. }` must omit the
13084 // key entirely (the `.is_empty()`-gated inner arm elides an
13085 // empty composite even when the outer presence bit is `Some`),
13086 // and a `Caixa { limits: None, .. }` must also omit the key
13087 // (the "author omitted the slot entirely" partition). The
13088 // three-fixture family jointly pins the accessor + M2 overlay
13089 // emitter composition: any future silent detour that had the
13090 // accessor return a fresh-cloned copy on the `Some` arm (a
13091 // `LimitsSpec::clone()` projection) would silently break the
13092 // reference-identity pin the peer per-axis
13093 // `serde_yaml::to_value(limits)` projection reads from.
13094 use crate::LimitsSpec;
13095 use crate::render::{M2_KEY_LIMITS, servico_m2_overlay};
13096 let c = caixa_with_limits(Some(LimitsSpec {
13097 memory: Some(64 * 1024 * 1024),
13098 ..Default::default()
13099 }));
13100 let overlay = servico_m2_overlay(&c).unwrap();
13101 assert!(
13102 overlay.contains_key(M2_KEY_LIMITS),
13103 "servico_m2_overlay must surface M2_KEY_LIMITS when \
13104 `:limits` carries a non-empty composite — the accessor \
13105 and the M2 overlay emitter must route through the same \
13106 substrate-primitive typed dispatch on the outer :limits \
13107 composite (got overlay={overlay:?})",
13108 );
13109 let c = caixa_with_limits(Some(LimitsSpec::default()));
13110 let overlay = servico_m2_overlay(&c).unwrap();
13111 assert!(
13112 !overlay.contains_key(M2_KEY_LIMITS),
13113 "servico_m2_overlay must omit M2_KEY_LIMITS when \
13114 `:limits` is Some(LimitsSpec::default()) — the empty \
13115 composite's `.is_empty()`-gated inner arm must elide \
13116 the key regardless of the outer presence bit (got \
13117 overlay={overlay:?})",
13118 );
13119 let c = caixa_with_limits(None);
13120 let overlay = servico_m2_overlay(&c).unwrap();
13121 assert!(
13122 !overlay.contains_key(M2_KEY_LIMITS),
13123 "servico_m2_overlay must omit M2_KEY_LIMITS when \
13124 `:limits` is None — the author-omitted arm must route \
13125 through the accessor's None-return unchanged (got \
13126 overlay={overlay:?})",
13127 );
13128 }
13129
13130 #[test]
13131 fn limits_projects_option_ref_by_borrow() {
13132 // The by-borrow pin: [`Caixa::limits`] returns
13133 // `Option<&LimitsSpec>` by borrow — the returned reference
13134 // borrows the underlying `Option<LimitsSpec>` storage of the
13135 // `:limits` slot and the accessor must not clone the backing
13136 // composite on every call. Peer of the sibling
13137 // `deps_projects_slice_by_borrow` (ad34b4e) /
13138 // `deps_dev_projects_slice_by_borrow` (f7fd81e) by-borrow pins
13139 // on the outer top-level [`Caixa`] `&[Dep]`-return axes —
13140 // extended here to the outer [`Caixa`] `Option<&Composite>`-
13141 // return axis: the accessor's returned reference must borrow
13142 // from `&self` (the returned reference's lifetime is tied to
13143 // `&self`), and calling the accessor twice on the same
13144 // [`Caixa`] must yield references that are pointer-equal (the
13145 // underlying byte-buffer is the storage `LimitsSpec`'s
13146 // allocation, not a fresh copy) as well as value-equal
13147 // (idempotent, no side effects on `&self`).
13148 //
13149 // Pins against a future silent detour that returned an owned
13150 // `LimitsSpec` (which would type-check via the `Clone` impl
13151 // but silently clone on every call), a `&LimitsSpec` panic-
13152 // return on the `None` arm (which would collapse the load-
13153 // bearing `Option` presence-bit into a runtime panic), or a
13154 // one-arm-only accessor that returned a saturating composite
13155 // on some sentinel input.
13156 use crate::LimitsSpec;
13157 use std::time::Duration;
13158 for limits in [
13159 Some(LimitsSpec::default()),
13160 Some(LimitsSpec {
13161 memory: Some(64 * 1024 * 1024),
13162 fuel: Some(1_000_000),
13163 wall_clock: Some(Duration::from_secs(30)),
13164 cpu: Some(500),
13165 }),
13166 ] {
13167 let c = caixa_with_limits(limits.clone());
13168 let first = c.limits().unwrap();
13169 let second = c.limits().unwrap();
13170 assert_eq!(
13171 first, second,
13172 "Caixa::limits must be idempotent — two successive \
13173 calls on the same &self must return the same \
13174 &LimitsSpec",
13175 );
13176 assert!(
13177 std::ptr::eq(first, second),
13178 "Caixa::limits must borrow the underlying \
13179 Option<LimitsSpec> storage — two successive calls \
13180 must return references with the same backing pointer \
13181 (a fresh LimitsSpec clone would change the pointer \
13182 on every call)",
13183 );
13184 assert_eq!(
13185 Some(first),
13186 limits.as_ref(),
13187 "Caixa::limits must return :limits verbatim by borrow \
13188 — got {first:?}, expected {:?}",
13189 limits.as_ref(),
13190 );
13191 }
13192 let c = caixa_with_limits(None);
13193 assert!(
13194 c.limits().is_none(),
13195 "Caixa::limits must return None when :limits is absent — \
13196 the author-omitted arm must project through the \
13197 accessor's Option::None unchanged",
13198 );
13199 }
13200
13201 // ── Caixa::behavior — outer top-level Option<&BehaviorSpec> composite-reference accessor ──
13202
13203 fn caixa_with_behavior(behavior: Option<crate::BehaviorSpec>) -> Caixa {
13204 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
13205 c.behavior = behavior;
13206 c
13207 }
13208
13209 #[test]
13210 fn behavior_returns_behavior_option_ref_verbatim_across_permutations() {
13211 // The canonical per-`Caixa` `:behavior` M2 typed-slot outer-
13212 // composite optional-composite-reference-shape pin:
13213 // [`Caixa::behavior`] must return the `:behavior` typed
13214 // `Option<BehaviorSpec>` verbatim as an `Option<&BehaviorSpec>`
13215 // reference over the same backing storage the raw
13216 // `self.behavior.as_ref()` field access borrows from, byte-equal
13217 // across every representative fixture in the accept-set — the
13218 // author-omitted `None` shape (the "runtime-default applies"
13219 // partition every downstream Servico M2 overlay emitter treats
13220 // as "emit nothing"), the empty-composite `Some(BehaviorSpec {
13221 // .. default })` shape ([`BehaviorSpec::is_empty`] holds —
13222 // every per-callback path is `None`, so the peer M2 overlay
13223 // emitter's `.is_empty()`-gated projection still emits nothing
13224 // but the outer presence-bit is `Some`, so
13225 // [`Caixa::declared_servico_slots`] still pushes the
13226 // `M2_AUTHOR_KEY_BEHAVIOR` label), a single-callback fixture
13227 // (only `:on-state-change` set — the canonical shape a caixa
13228 // that only wires the hot-upgrade migration path carries), and
13229 // a fully-populated composite (every per-callback path set —
13230 // the canonical shape a fully-instrumented gen_server-shaped
13231 // Servico carries).
13232 //
13233 // Peer of the sibling
13234 // `limits_returns_limits_option_ref_verbatim_across_permutations`
13235 // (b2bd9d7) opening fixture-family + reference-identity +
13236 // presence-bit tetrad pin on the outer top-level [`Caixa`]
13237 // `Option<&Composite>`-return sub-family — extended here to the
13238 // second axis of that sub-family so both of the currently-lifted
13239 // M2 Servico-runtime `Option<&Composite>` slots (`:limits` /
13240 // `:behavior`) carry the same "byte-equal, borrow-shared,
13241 // presence-bit-preserved" outer-accessor discipline.
13242 //
13243 // Pins against a future silent detour that returned a fresh-
13244 // cloned [`crate::BehaviorSpec`] copy (which would type-check
13245 // via the `Clone` impl but silently break every downstream
13246 // caller that relied on the reference sharing the composite's
13247 // backing identity), a reference to an operator-resolved
13248 // overlay (a future per-cluster `:behavior-overrides` slot —
13249 // its resolution must land at exactly this accessor body, not
13250 // silently divert the raw slot away from a second consumer), a
13251 // `None` → `Some(BehaviorSpec::default)` cluster-default
13252 // projection (which would collapse the load-bearing
13253 // "author-omitted `:behavior` ⇒ runtime-default applies"
13254 // partition the peer [`crate::render::servico_m2_overlay`]
13255 // emitter, the peer [`Caixa::declared_servico_slots`]
13256 // enumerator, and the cross-slot
13257 // [`crate::upgrade::validate_upgrade_from_against_behavior`]
13258 // gate all read), or a callback-shuffled projection (a future
13259 // detour that swapped `on_init` and `on_terminate` through the
13260 // accessor would silently split the paired
13261 // [`crate::StandardLayout::verify`] per-`:behavior` shape gate's
13262 // traversal input from the peer `servico_m2_overlay` emitter's
13263 // projection input from the cross-slot `:state-change`
13264 // composition gate's traversal input).
13265 use crate::BehaviorSpec;
13266 use std::path::PathBuf;
13267 let fixtures: Vec<Option<BehaviorSpec>> = vec![
13268 None,
13269 Some(BehaviorSpec::default()),
13270 Some(BehaviorSpec {
13271 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
13272 ..Default::default()
13273 }),
13274 Some(BehaviorSpec {
13275 on_init: Some(PathBuf::from("lib/init.lisp")),
13276 on_call: Some(PathBuf::from("lib/handlers.lisp")),
13277 on_cast: Some(PathBuf::from("lib/handlers.lisp")),
13278 on_info: Some(PathBuf::from("lib/handlers.lisp")),
13279 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
13280 on_terminate: Some(PathBuf::from("lib/cleanup.lisp")),
13281 }),
13282 ];
13283 for behavior in fixtures {
13284 let c = caixa_with_behavior(behavior.clone());
13285 assert_eq!(
13286 c.behavior(),
13287 behavior.as_ref(),
13288 "Caixa::behavior must return :behavior verbatim (got \
13289 {:?}, expected {:?})",
13290 c.behavior(),
13291 behavior.as_ref(),
13292 );
13293 match (c.behavior(), c.behavior.as_ref()) {
13294 (Some(a), Some(b)) => assert!(
13295 std::ptr::eq(a, b),
13296 "Caixa::behavior accessor and self.behavior.as_ref() \
13297 field access must borrow the same backing storage \
13298 — the accessor is the substrate-primitive typed \
13299 dispatch every downstream Servico-M2-overlay \
13300 composite consumer must route through, and a \
13301 reference-identity split would silently break \
13302 every consumer that relied on the borrow sharing \
13303 the composite's storage",
13304 ),
13305 (None, None) => {}
13306 _ => panic!(
13307 "Caixa::behavior presence bit must byte-equal \
13308 self.behavior.is_some() — a presence-bit drift \
13309 would silently split the paired \
13310 StandardLayout::verify per-`:behavior` shape \
13311 gate's traversal head from the peer \
13312 render::servico_m2_overlay M2 overlay emitter's \
13313 traversal head from the cross-slot \
13314 validate_upgrade_from_against_behavior \
13315 composition gate's traversal head from the peer \
13316 Caixa::declared_servico_slots M2 declared-slot \
13317 enumerator's presence probe",
13318 ),
13319 }
13320 assert_eq!(
13321 c.behavior().is_some(),
13322 c.behavior.is_some(),
13323 "Caixa::behavior().is_some() must byte-equal \
13324 self.behavior.is_some() — a presence-bit drift would \
13325 silently split every downstream Option<&BehaviorSpec> \
13326 consumer's partition on the runtime-default arm",
13327 );
13328 }
13329 }
13330
13331 #[test]
13332 fn declared_servico_slots_behavior_arm_routes_through_accessor() {
13333 // Composition pin: [`Caixa::declared_servico_slots`]'s
13334 // `:behavior` presence-probe arm must key off
13335 // [`Caixa::behavior`], not the raw `self.behavior.is_some()`
13336 // field-probe. Structurally: a `Caixa { behavior:
13337 // Some(BehaviorSpec::default()), .. }` must still push
13338 // `M2_AUTHOR_KEY_BEHAVIOR` onto the declared-slot list (the
13339 // presence bit is `Some`, so the M2 kind-coherence gate must
13340 // surface the slot as "declared" even when every per-callback
13341 // path is unset), and a `Caixa { behavior: None, .. }` must
13342 // NOT push the label (the "author omitted the slot entirely"
13343 // partition). The pair jointly pins the accessor + declared-
13344 // slot enumerator composition: any future silent detour that
13345 // had the accessor collapse `Some(BehaviorSpec::default())`
13346 // to `None` (a `.filter(|b| !b.is_empty())` projection) would
13347 // silently absorb the "declared but empty" arm at the
13348 // accessor boundary and the
13349 // [`crate::LayoutError::ServicoSlotsOnNonServico`]
13350 // kind-coherence gate would silently accept a struct-literal
13351 // `Caixa` carrying the drift.
13352 //
13353 // Peer of the sibling
13354 // `declared_servico_slots_limits_arm_routes_through_accessor`
13355 // (b2bd9d7) composition pin on the sibling `:limits` outer-
13356 // `Option<&LimitsSpec>` arm of the same
13357 // [`Caixa::declared_servico_slots`] M2 declared-slot
13358 // enumerator's traversal — same "the enumerator gate must
13359 // route through the substrate-primitive typed dispatch"
13360 // discipline extended onto the outer top-level [`Caixa`]
13361 // `Option<&BehaviorSpec>`-composition surface.
13362 use crate::BehaviorSpec;
13363 let c = caixa_with_behavior(Some(BehaviorSpec::default()));
13364 let slots = c.declared_servico_slots();
13365 assert!(
13366 slots.contains(&crate::render::M2_AUTHOR_KEY_BEHAVIOR),
13367 "declared_servico_slots must push M2_AUTHOR_KEY_BEHAVIOR \
13368 when `:behavior` is Some (even for BehaviorSpec::default()) \
13369 — the accessor and the enumerator gate must route through \
13370 the same substrate-primitive typed dispatch on the outer \
13371 :behavior presence bit (got slots={slots:?})",
13372 );
13373 let c = caixa_with_behavior(None);
13374 let slots = c.declared_servico_slots();
13375 assert!(
13376 !slots.contains(&crate::render::M2_AUTHOR_KEY_BEHAVIOR),
13377 "declared_servico_slots must NOT push M2_AUTHOR_KEY_BEHAVIOR \
13378 when `:behavior` is None — the author-omitted arm must \
13379 route through the accessor's None-return unchanged (got \
13380 slots={slots:?})",
13381 );
13382 }
13383
13384 #[test]
13385 fn servico_m2_overlay_behavior_arm_routes_through_accessor() {
13386 // Composition pin: [`crate::render::servico_m2_overlay`]'s
13387 // per-`:behavior` M2 overlay emit arm must key off
13388 // [`Caixa::behavior`], not the raw `&caixa.behavior`
13389 // field-borrow. Structurally: a `Caixa { behavior:
13390 // Some(BehaviorSpec { on_state_change: Some(...), .. default
13391 // }), .. }` must surface the `M2_KEY_BEHAVIOR` key with the
13392 // per-callback `onStateChange` sub-mapping in the overlay, a
13393 // `Caixa { behavior: Some(BehaviorSpec::default()), .. }`
13394 // must omit the key entirely (the `.is_empty()`-gated inner
13395 // arm elides an empty composite even when the outer presence
13396 // bit is `Some`), and a `Caixa { behavior: None, .. }` must
13397 // also omit the key (the "author omitted the slot entirely"
13398 // partition). The three-fixture family jointly pins the
13399 // accessor + M2 overlay emitter composition: any future
13400 // silent detour that had the accessor return a fresh-cloned
13401 // copy on the `Some` arm (a `BehaviorSpec::clone()`
13402 // projection) would silently break the reference-identity
13403 // pin the peer per-callback `serde_yaml::to_value(behavior)`
13404 // projection reads from.
13405 //
13406 // Peer of the sibling
13407 // `servico_m2_overlay_limits_arm_routes_through_accessor`
13408 // (b2bd9d7) composition pin on the sibling `:limits` outer-
13409 // `Option<&LimitsSpec>` arm of the same
13410 // [`crate::render::servico_m2_overlay`] M2 overlay emitter's
13411 // traversal — same "the emitter must route through the
13412 // substrate-primitive typed dispatch on the outer composite"
13413 // discipline extended onto the outer top-level [`Caixa`]
13414 // `Option<&BehaviorSpec>`-composition surface.
13415 use crate::BehaviorSpec;
13416 use crate::render::{M2_KEY_BEHAVIOR, servico_m2_overlay};
13417 use std::path::PathBuf;
13418 let c = caixa_with_behavior(Some(BehaviorSpec {
13419 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
13420 ..Default::default()
13421 }));
13422 let overlay = servico_m2_overlay(&c).unwrap();
13423 assert!(
13424 overlay.contains_key(M2_KEY_BEHAVIOR),
13425 "servico_m2_overlay must surface M2_KEY_BEHAVIOR when \
13426 `:behavior` carries a non-empty composite — the accessor \
13427 and the M2 overlay emitter must route through the same \
13428 substrate-primitive typed dispatch on the outer :behavior \
13429 composite (got overlay={overlay:?})",
13430 );
13431 let c = caixa_with_behavior(Some(BehaviorSpec::default()));
13432 let overlay = servico_m2_overlay(&c).unwrap();
13433 assert!(
13434 !overlay.contains_key(M2_KEY_BEHAVIOR),
13435 "servico_m2_overlay must omit M2_KEY_BEHAVIOR when \
13436 `:behavior` is Some(BehaviorSpec::default()) — the empty \
13437 composite's `.is_empty()`-gated inner arm must elide the \
13438 key regardless of the outer presence bit (got \
13439 overlay={overlay:?})",
13440 );
13441 let c = caixa_with_behavior(None);
13442 let overlay = servico_m2_overlay(&c).unwrap();
13443 assert!(
13444 !overlay.contains_key(M2_KEY_BEHAVIOR),
13445 "servico_m2_overlay must omit M2_KEY_BEHAVIOR when \
13446 `:behavior` is None — the author-omitted arm must route \
13447 through the accessor's None-return unchanged (got \
13448 overlay={overlay:?})",
13449 );
13450 }
13451
13452 #[test]
13453 fn behavior_projects_option_ref_by_borrow() {
13454 // The by-borrow pin: [`Caixa::behavior`] returns
13455 // `Option<&BehaviorSpec>` by borrow — the returned reference
13456 // borrows the underlying `Option<BehaviorSpec>` storage of the
13457 // `:behavior` slot and the accessor must not clone the backing
13458 // composite on every call. Peer of the sibling
13459 // `limits_projects_option_ref_by_borrow` (b2bd9d7) by-borrow
13460 // pin on the outer top-level [`Caixa`] `Option<&Composite>`-
13461 // return sub-family — extended here to the second axis of the
13462 // same sub-family: the accessor's returned reference must
13463 // borrow from `&self` (the returned reference's lifetime is
13464 // tied to `&self`), and calling the accessor twice on the same
13465 // [`Caixa`] must yield references that are pointer-equal (the
13466 // underlying byte-buffer is the storage `BehaviorSpec`'s
13467 // allocation, not a fresh copy) as well as value-equal
13468 // (idempotent, no side effects on `&self`).
13469 //
13470 // Pins against a future silent detour that returned an owned
13471 // `BehaviorSpec` (which would type-check via the `Clone` impl
13472 // but silently clone on every call), a `&BehaviorSpec` panic-
13473 // return on the `None` arm (which would collapse the load-
13474 // bearing `Option` presence-bit into a runtime panic), or a
13475 // one-arm-only accessor that returned a saturating composite
13476 // on some sentinel input.
13477 use crate::BehaviorSpec;
13478 use std::path::PathBuf;
13479 for behavior in [
13480 Some(BehaviorSpec::default()),
13481 Some(BehaviorSpec {
13482 on_init: Some(PathBuf::from("lib/init.lisp")),
13483 on_call: Some(PathBuf::from("lib/handlers.lisp")),
13484 on_cast: Some(PathBuf::from("lib/handlers.lisp")),
13485 on_info: Some(PathBuf::from("lib/handlers.lisp")),
13486 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
13487 on_terminate: Some(PathBuf::from("lib/cleanup.lisp")),
13488 }),
13489 ] {
13490 let c = caixa_with_behavior(behavior.clone());
13491 let first = c.behavior().unwrap();
13492 let second = c.behavior().unwrap();
13493 assert_eq!(
13494 first, second,
13495 "Caixa::behavior must be idempotent — two successive \
13496 calls on the same &self must return the same \
13497 &BehaviorSpec",
13498 );
13499 assert!(
13500 std::ptr::eq(first, second),
13501 "Caixa::behavior must borrow the underlying \
13502 Option<BehaviorSpec> storage — two successive calls \
13503 must return references with the same backing pointer \
13504 (a fresh BehaviorSpec clone would change the pointer \
13505 on every call)",
13506 );
13507 assert_eq!(
13508 Some(first),
13509 behavior.as_ref(),
13510 "Caixa::behavior must return :behavior verbatim by \
13511 borrow — got {first:?}, expected {:?}",
13512 behavior.as_ref(),
13513 );
13514 }
13515 let c = caixa_with_behavior(None);
13516 assert!(
13517 c.behavior().is_none(),
13518 "Caixa::behavior must return None when :behavior is absent \
13519 — the author-omitted arm must project through the \
13520 accessor's Option::None unchanged",
13521 );
13522 }
13523
13524 // ── Caixa::politicas — outer top-level Option<&MeshPolicy> composite-reference accessor ──
13525
13526 fn caixa_aplicacao_with_politicas(politicas: Option<crate::aplicacao::MeshPolicy>) -> Caixa {
13527 use crate::aplicacao::{Membro, WitContract};
13528 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
13529 c.kind = CaixaKind::Aplicacao;
13530 c.membros = vec![Membro {
13531 caixa: "a".into(),
13532 versao: "^0.1".into(),
13533 }];
13534 c.contratos = vec![WitContract {
13535 de: "a".into(),
13536 para: "a".into(),
13537 wit: "wasi:http/proxy".into(),
13538 endpoint: Some("/x".into()),
13539 subject: None,
13540 slot: None,
13541 }];
13542 c.politicas = politicas;
13543 c
13544 }
13545
13546 #[test]
13547 fn politicas_returns_politicas_option_ref_verbatim_across_permutations() {
13548 // The canonical per-`Caixa` `:politicas` M3 mesh-slot outer-
13549 // composite optional-composite-reference-shape pin:
13550 // [`Caixa::politicas`] must return the `:politicas` typed
13551 // `Option<MeshPolicy>` verbatim as an `Option<&MeshPolicy>`
13552 // reference over the same backing storage the raw
13553 // `self.politicas.as_ref()` field access borrows from,
13554 // byte-equal across every representative fixture in the
13555 // accept-set — the author-omitted `None` shape (the "cluster-
13556 // default applies" partition every downstream mesh-artifact
13557 // emitter treats as "emit no `:politicas` overlay"), the
13558 // empty-composite `Some(MeshPolicy { .. default })` shape
13559 // ([`crate::aplicacao::MeshPolicy::is_empty`] holds — every
13560 // per-axis mesh-policy scalar is `None`, so the peer inner
13561 // [`crate::AplicacaoSpec::politicas`] `.is_empty()`-gated
13562 // caixa-mesh overlay elides every per-axis emit but the outer
13563 // presence-bit is `Some`, so [`Caixa::declared_mesh_slots`]
13564 // still pushes the `M3_AUTHOR_KEY_POLITICAS` label), a
13565 // single-axis fixture (only `:timeout` set — the canonical
13566 // shape a latency-sensitive Aplicacao carries), and a
13567 // fully-populated composite (every per-axis mesh-policy
13568 // scalar set — the canonical shape a fully-governed
13569 // Aplicacao carries).
13570 //
13571 // Pins against a future silent detour that returned a fresh-
13572 // cloned [`crate::aplicacao::MeshPolicy`] copy (which would
13573 // type-check via the `Clone` impl but silently break every
13574 // downstream caller that relied on the reference sharing the
13575 // composite's backing identity), a reference to an operator-
13576 // resolved overlay (the future per-cluster
13577 // `:politicas-overrides` slot — its resolution must land at
13578 // exactly this accessor body, not silently divert the raw
13579 // slot away from the peer [`Caixa::declared_mesh_slots`]
13580 // enumerator's presence probe), a
13581 // `None` → `Some(MeshPolicy::default)` cluster-default
13582 // projection (which would collapse the load-bearing
13583 // "author-omitted `:politicas` ⇒ cluster-default applies"
13584 // partition the peer [`Caixa::declared_mesh_slots`]
13585 // enumerator and the peer [`Caixa::aplicacao_view`]
13586 // Aplicacao-composition seed both read), or an axis-shuffled
13587 // projection (a future detour that swapped `timeout` and
13588 // `retries` through the accessor would silently split the
13589 // paired [`Caixa::aplicacao_view`] seed's fold input from the
13590 // sibling M3 mesh-artifact emitter's projection input).
13591 //
13592 // Third outer top-level [`Caixa`] `Option<&Composite>`-return
13593 // composite-reference accessor pin on the substrate primitive
13594 // — peer of the sibling
13595 // `limits_returns_limits_option_ref_verbatim_across_permutations`
13596 // (b2bd9d7) and
13597 // `behavior_returns_behavior_option_ref_verbatim_across_permutations`
13598 // (35d8b52) opening tetrad pins on the outer top-level
13599 // [`Caixa`] `Option<&Composite>`-return sub-family — extended
13600 // here to the first of the three M3 mesh-slot axes so the
13601 // opening third of the outer `Option<&Composite>` sub-family
13602 // carries the same "byte-equal, borrow-shared, presence-bit-
13603 // preserved" outer-accessor discipline.
13604 use crate::aplicacao::{CircuitBreaker, MeshPolicy, RateLimit};
13605 use std::time::Duration;
13606 let fixtures: Vec<Option<MeshPolicy>> = vec![
13607 None,
13608 Some(MeshPolicy::default()),
13609 Some(MeshPolicy {
13610 timeout: Some(Duration::from_secs(30)),
13611 ..Default::default()
13612 }),
13613 Some(MeshPolicy {
13614 timeout: Some(Duration::from_secs(30)),
13615 retries: Some(3),
13616 circuit_breaker: Some(CircuitBreaker {
13617 max_failures: 5,
13618 window: Duration::from_secs(60),
13619 }),
13620 mtls_required: Some(true),
13621 rate_limit: Some(RateLimit {
13622 rate: 100,
13623 window: Duration::from_secs(1),
13624 }),
13625 }),
13626 ];
13627 for politicas in fixtures {
13628 let c = caixa_aplicacao_with_politicas(politicas.clone());
13629 assert_eq!(
13630 c.politicas(),
13631 politicas.as_ref(),
13632 "Caixa::politicas must return :politicas verbatim (got \
13633 {:?}, expected {:?})",
13634 c.politicas(),
13635 politicas.as_ref(),
13636 );
13637 match (c.politicas(), c.politicas.as_ref()) {
13638 (Some(a), Some(b)) => assert!(
13639 std::ptr::eq(a, b),
13640 "Caixa::politicas accessor and self.politicas.as_ref() \
13641 field access must borrow the same backing storage \
13642 — the accessor is the substrate-primitive typed \
13643 dispatch every downstream Aplicacao-mesh-overlay \
13644 composite consumer must route through, and a \
13645 reference-identity split would silently break \
13646 every consumer that relied on the borrow sharing \
13647 the composite's storage",
13648 ),
13649 (None, None) => {}
13650 _ => panic!(
13651 "Caixa::politicas presence bit must byte-equal \
13652 self.politicas.is_some() — a presence-bit drift \
13653 would silently split the paired \
13654 Caixa::aplicacao_view Aplicacao-composition seed's \
13655 traversal head from the peer \
13656 Caixa::declared_mesh_slots M3 declared-slot \
13657 enumerator's presence probe",
13658 ),
13659 }
13660 assert_eq!(
13661 c.politicas().is_some(),
13662 c.politicas.is_some(),
13663 "Caixa::politicas().is_some() must byte-equal \
13664 self.politicas.is_some() — a presence-bit drift would \
13665 silently split every downstream Option<&MeshPolicy> \
13666 consumer's partition on the cluster-default arm",
13667 );
13668 }
13669 }
13670
13671 #[test]
13672 fn declared_mesh_slots_politicas_arm_routes_through_accessor() {
13673 // Composition pin: [`Caixa::declared_mesh_slots`]'s
13674 // `:politicas` presence-probe arm must key off
13675 // [`Caixa::politicas`], not the raw `self.politicas.is_some()`
13676 // field-probe. Structurally: a `Caixa { politicas:
13677 // Some(MeshPolicy::default()), .. }` must still push
13678 // `M3_AUTHOR_KEY_POLITICAS` onto the declared-slot list (the
13679 // presence bit is `Some`, so the M3 kind-coherence gate must
13680 // surface the slot as "declared" even when every per-axis
13681 // scalar is unset), and a `Caixa { politicas: None, .. }` must
13682 // NOT push the label (the "author omitted the slot entirely"
13683 // partition). The pair jointly pins the accessor + declared-
13684 // slot enumerator composition: any future silent detour that
13685 // had the accessor collapse `Some(MeshPolicy::default())` to
13686 // `None` (a `.filter(|p| !p.is_empty())` projection) would
13687 // silently absorb the "declared but empty" arm at the
13688 // accessor boundary and the
13689 // [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
13690 // coherence gate would silently accept a struct-literal
13691 // `Caixa` carrying the drift.
13692 //
13693 // Peer of the sibling
13694 // `declared_servico_slots_limits_arm_routes_through_accessor`
13695 // (b2bd9d7) and
13696 // `declared_servico_slots_behavior_arm_routes_through_accessor`
13697 // (35d8b52) composition pins on the sibling `:limits` /
13698 // `:behavior` outer-`Option<&Composite>` arms of the peer
13699 // [`Caixa::declared_servico_slots`] M2 declared-slot
13700 // enumerator's traversal — same "the enumerator gate must
13701 // route through the substrate-primitive typed dispatch"
13702 // discipline extended onto the outer top-level [`Caixa`] M3
13703 // mesh-slot family so the [`Caixa::declared_mesh_slots`]
13704 // enumerator carries the same routing invariant as its M2
13705 // sibling.
13706 use crate::aplicacao::MeshPolicy;
13707 let c = caixa_aplicacao_with_politicas(Some(MeshPolicy::default()));
13708 let slots = c.declared_mesh_slots();
13709 assert!(
13710 slots.contains(&crate::render::M3_AUTHOR_KEY_POLITICAS),
13711 "declared_mesh_slots must push M3_AUTHOR_KEY_POLITICAS \
13712 when `:politicas` is Some (even for MeshPolicy::default()) \
13713 — the accessor and the enumerator gate must route through \
13714 the same substrate-primitive typed dispatch on the outer \
13715 :politicas presence bit (got slots={slots:?})",
13716 );
13717 let c = caixa_aplicacao_with_politicas(None);
13718 let slots = c.declared_mesh_slots();
13719 assert!(
13720 !slots.contains(&crate::render::M3_AUTHOR_KEY_POLITICAS),
13721 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_POLITICAS \
13722 when `:politicas` is None — the author-omitted arm must \
13723 route through the accessor's None-return unchanged (got \
13724 slots={slots:?})",
13725 );
13726 }
13727
13728 #[test]
13729 fn aplicacao_view_politicas_arm_folds_through_accessor() {
13730 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:politicas`
13731 // Aplicacao-composition seed must fold through
13732 // [`Caixa::politicas`], not the raw
13733 // `self.politicas.clone().unwrap_or_default()` field-borrow.
13734 // Structurally: a `Caixa { politicas: Some(MeshPolicy {
13735 // timeout: Some(30s), .. default }), kind: Aplicacao, .. }`
13736 // must surface a projected [`crate::AplicacaoSpec`] whose
13737 // `politicas().timeout()` field byte-equals the outer
13738 // composite's `timeout` scalar (the fold must project the
13739 // authored composite verbatim), a `Caixa { politicas:
13740 // Some(MeshPolicy::default()), kind: Aplicacao, .. }` must
13741 // surface an [`crate::AplicacaoSpec`] whose `politicas()`
13742 // byte-equals [`crate::aplicacao::MeshPolicy::default`] (the
13743 // fold's empty-composite arm collapses to the same default the
13744 // author-omitted arm does), and a `Caixa { politicas: None,
13745 // kind: Aplicacao, .. }` must surface an
13746 // [`crate::AplicacaoSpec`] whose `politicas()` byte-equals
13747 // [`crate::aplicacao::MeshPolicy::default`] (the "author
13748 // omitted the slot entirely" arm folds through the
13749 // `unwrap_or_default` onto the cluster-default). The triad
13750 // jointly pins the accessor + Aplicacao-composition seed
13751 // composition: any future silent detour that had the accessor
13752 // divert the raw slot away from the seed's fold (an operator-
13753 // resolved overlay's default-fold arm silently differing from
13754 // the raw slot's default-fold arm) would silently split the
13755 // build-time mesh-artifact emission gate from the caixa-mesh
13756 // renderer's Aplicacao-view input at the composition boundary.
13757 use crate::aplicacao::MeshPolicy;
13758 use std::time::Duration;
13759 let c = caixa_aplicacao_with_politicas(Some(MeshPolicy {
13760 timeout: Some(Duration::from_secs(30)),
13761 ..Default::default()
13762 }));
13763 let view = c.aplicacao_view().unwrap();
13764 assert_eq!(
13765 view.politicas().timeout(),
13766 Some(Duration::from_secs(30)),
13767 "Caixa::aplicacao_view must fold the authored :politicas \
13768 :timeout scalar through the accessor verbatim onto the \
13769 projected AplicacaoSpec — a future silent detour at the \
13770 seed's fold arm would surface here as a projected-scalar \
13771 drift (got {:?})",
13772 view.politicas().timeout(),
13773 );
13774 let c = caixa_aplicacao_with_politicas(Some(MeshPolicy::default()));
13775 let view = c.aplicacao_view().unwrap();
13776 assert_eq!(
13777 view.politicas(),
13778 &MeshPolicy::default(),
13779 "Caixa::aplicacao_view must fold Some(MeshPolicy::default()) \
13780 through the accessor onto MeshPolicy::default — the empty- \
13781 composite arm collapses to the same default the author- \
13782 omitted arm does (got {:?})",
13783 view.politicas(),
13784 );
13785 let c = caixa_aplicacao_with_politicas(None);
13786 let view = c.aplicacao_view().unwrap();
13787 assert_eq!(
13788 view.politicas(),
13789 &MeshPolicy::default(),
13790 "Caixa::aplicacao_view must fold None through the accessor's \
13791 unwrap_or_default onto MeshPolicy::default — the author- \
13792 omitted arm must route through the accessor's None-return \
13793 unchanged (got {:?})",
13794 view.politicas(),
13795 );
13796 }
13797
13798 #[test]
13799 fn politicas_projects_option_ref_by_borrow() {
13800 // The by-borrow pin: [`Caixa::politicas`] returns
13801 // `Option<&MeshPolicy>` by borrow — the returned reference
13802 // borrows the underlying `Option<MeshPolicy>` storage of the
13803 // `:politicas` slot and the accessor must not clone the
13804 // backing composite on every call. Peer of the sibling
13805 // `limits_projects_option_ref_by_borrow` (b2bd9d7) and
13806 // `behavior_projects_option_ref_by_borrow` (35d8b52) by-borrow
13807 // pins on the outer top-level [`Caixa`]
13808 // `Option<&Composite>`-return sub-family — extended here to
13809 // the third axis of the same sub-family: the accessor's
13810 // returned reference must borrow from `&self` (the returned
13811 // reference's lifetime is tied to `&self`), and calling the
13812 // accessor twice on the same [`Caixa`] must yield references
13813 // that are pointer-equal (the underlying byte-buffer is the
13814 // storage `MeshPolicy`'s allocation, not a fresh copy) as
13815 // well as value-equal (idempotent, no side effects on
13816 // `&self`).
13817 //
13818 // Pins against a future silent detour that returned an owned
13819 // `MeshPolicy` (which would type-check via the `Clone` impl
13820 // but silently clone on every call), a `&MeshPolicy` panic-
13821 // return on the `None` arm (which would collapse the load-
13822 // bearing `Option` presence-bit into a runtime panic), or a
13823 // one-arm-only accessor that returned a saturating composite
13824 // on some sentinel input.
13825 use crate::aplicacao::{CircuitBreaker, MeshPolicy, RateLimit};
13826 use std::time::Duration;
13827 for politicas in [
13828 Some(MeshPolicy::default()),
13829 Some(MeshPolicy {
13830 timeout: Some(Duration::from_secs(30)),
13831 retries: Some(3),
13832 circuit_breaker: Some(CircuitBreaker {
13833 max_failures: 5,
13834 window: Duration::from_secs(60),
13835 }),
13836 mtls_required: Some(true),
13837 rate_limit: Some(RateLimit {
13838 rate: 100,
13839 window: Duration::from_secs(1),
13840 }),
13841 }),
13842 ] {
13843 let c = caixa_aplicacao_with_politicas(politicas.clone());
13844 let first = c.politicas().unwrap();
13845 let second = c.politicas().unwrap();
13846 assert_eq!(
13847 first, second,
13848 "Caixa::politicas must be idempotent — two successive \
13849 calls on the same &self must return the same \
13850 &MeshPolicy",
13851 );
13852 assert!(
13853 std::ptr::eq(first, second),
13854 "Caixa::politicas must borrow the underlying \
13855 Option<MeshPolicy> storage — two successive calls \
13856 must return references with the same backing pointer \
13857 (a fresh MeshPolicy clone would change the pointer on \
13858 every call)",
13859 );
13860 assert_eq!(
13861 Some(first),
13862 politicas.as_ref(),
13863 "Caixa::politicas must return :politicas verbatim by \
13864 borrow — got {first:?}, expected {:?}",
13865 politicas.as_ref(),
13866 );
13867 }
13868 let c = caixa_aplicacao_with_politicas(None);
13869 assert!(
13870 c.politicas().is_none(),
13871 "Caixa::politicas must return None when :politicas is \
13872 absent — the author-omitted arm must project through the \
13873 accessor's Option::None unchanged",
13874 );
13875 }
13876
13877 // ── Caixa::placement — outer top-level Option<&Placement> composite-reference accessor ──
13878
13879 fn caixa_aplicacao_with_placement(placement: Option<crate::aplicacao::Placement>) -> Caixa {
13880 use crate::aplicacao::{Membro, WitContract};
13881 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
13882 c.kind = CaixaKind::Aplicacao;
13883 c.membros = vec![Membro {
13884 caixa: "a".into(),
13885 versao: "^0.1".into(),
13886 }];
13887 c.contratos = vec![WitContract {
13888 de: "a".into(),
13889 para: "a".into(),
13890 wit: "wasi:http/proxy".into(),
13891 endpoint: Some("/x".into()),
13892 subject: None,
13893 slot: None,
13894 }];
13895 c.placement = placement;
13896 c
13897 }
13898
13899 #[test]
13900 fn placement_returns_placement_option_ref_verbatim_across_permutations() {
13901 // The canonical per-`Caixa` `:placement` M3 mesh-slot outer-
13902 // composite optional-composite-reference-shape pin:
13903 // [`Caixa::placement`] must return the `:placement` typed
13904 // `Option<Placement>` verbatim as an `Option<&Placement>`
13905 // reference over the same backing storage the raw
13906 // `self.placement.as_ref()` field access borrows from,
13907 // byte-equal across every representative fixture in the
13908 // accept-set — the author-omitted `None` shape (the
13909 // "cluster-default applies" partition every downstream mesh-
13910 // artifact emitter treats as "emit no `:placement` overlay"),
13911 // the empty-composite `Some(Placement { .. default })` shape
13912 // (`estrategia: SingleNode`, empty clusters, no shard-key /
13913 // affinity — the outer presence-bit is `Some` so
13914 // [`Caixa::declared_mesh_slots`] still pushes the
13915 // `M3_AUTHOR_KEY_PLACEMENT` label), a single-axis
13916 // `Replicated`-on-two-clusters fixture (the canonical shape a
13917 // stateless HTTP Aplicacao carries), and a fully-populated
13918 // `Sharded`-with-shard-key-and-affinity fixture (the canonical
13919 // shape a stateful Akka-style cluster-sharding Aplicacao
13920 // carries).
13921 //
13922 // Pins against a future silent detour that returned a fresh-
13923 // cloned [`crate::aplicacao::Placement`] copy (which would
13924 // type-check via the `Clone` impl but silently break every
13925 // downstream caller that relied on the reference sharing the
13926 // composite's backing identity), a reference to an operator-
13927 // resolved overlay (the future per-cluster
13928 // `:placement-overrides` slot — its resolution must land at
13929 // exactly this accessor body, not silently divert the raw
13930 // slot away from the peer [`Caixa::declared_mesh_slots`]
13931 // enumerator's presence probe), a `None` →
13932 // `Some(Placement::default)` cluster-default projection (which
13933 // would collapse the load-bearing "author-omitted `:placement`
13934 // ⇒ cluster-default applies" partition the peer
13935 // [`Caixa::declared_mesh_slots`] enumerator and the peer
13936 // [`Caixa::aplicacao_view`] Aplicacao-composition seed both
13937 // read), or an axis-shuffled projection (a future detour that
13938 // swapped `clusters` and `affinity` through the accessor would
13939 // silently split the paired [`Caixa::aplicacao_view`] seed's
13940 // fold input from the sibling M3 mesh-artifact emitter's
13941 // projection input).
13942 //
13943 // Fourth outer top-level [`Caixa`] `Option<&Composite>`-return
13944 // composite-reference accessor pin on the substrate primitive
13945 // — peer of the sibling
13946 // `limits_returns_limits_option_ref_verbatim_across_permutations`
13947 // (b2bd9d7),
13948 // `behavior_returns_behavior_option_ref_verbatim_across_permutations`
13949 // (35d8b52), and
13950 // `politicas_returns_politicas_option_ref_verbatim_across_permutations`
13951 // (5d23d29) opening triad pins on the outer top-level
13952 // [`Caixa`] `Option<&Composite>`-return sub-family — extended
13953 // here to the second of the three M3 mesh-slot axes so the
13954 // opening four-fifths of the outer `Option<&Composite>` sub-
13955 // family carries the same "byte-equal, borrow-shared,
13956 // presence-bit-preserved" outer-accessor discipline.
13957 use crate::aplicacao::{Placement, PlacementStrategy};
13958 let fixtures: Vec<Option<Placement>> = vec![
13959 None,
13960 Some(Placement::default()),
13961 Some(Placement {
13962 estrategia: PlacementStrategy::Replicated,
13963 clusters: vec!["rio".into(), "sao-paulo".into()],
13964 affinity: None,
13965 shard_key: None,
13966 }),
13967 Some(Placement {
13968 estrategia: PlacementStrategy::Sharded,
13969 clusters: vec!["rio".into(), "sao-paulo".into(), "brasilia".into()],
13970 affinity: Some("data-locality".into()),
13971 shard_key: Some("$tenantId".into()),
13972 }),
13973 ];
13974 for placement in fixtures {
13975 let c = caixa_aplicacao_with_placement(placement.clone());
13976 assert_eq!(
13977 c.placement(),
13978 placement.as_ref(),
13979 "Caixa::placement must return :placement verbatim (got \
13980 {:?}, expected {:?})",
13981 c.placement(),
13982 placement.as_ref(),
13983 );
13984 match (c.placement(), c.placement.as_ref()) {
13985 (Some(a), Some(b)) => assert!(
13986 std::ptr::eq(a, b),
13987 "Caixa::placement accessor and self.placement.as_ref() \
13988 field access must borrow the same backing storage \
13989 — the accessor is the substrate-primitive typed \
13990 dispatch every downstream Aplicacao-distribution- \
13991 overlay composite consumer must route through, and \
13992 a reference-identity split would silently break \
13993 every consumer that relied on the borrow sharing \
13994 the composite's storage",
13995 ),
13996 (None, None) => {}
13997 _ => panic!(
13998 "Caixa::placement presence bit must byte-equal \
13999 self.placement.is_some() — a presence-bit drift \
14000 would silently split the paired \
14001 Caixa::aplicacao_view Aplicacao-composition seed's \
14002 traversal head from the peer \
14003 Caixa::declared_mesh_slots M3 declared-slot \
14004 enumerator's presence probe",
14005 ),
14006 }
14007 assert_eq!(
14008 c.placement().is_some(),
14009 c.placement.is_some(),
14010 "Caixa::placement().is_some() must byte-equal \
14011 self.placement.is_some() — a presence-bit drift would \
14012 silently split every downstream Option<&Placement> \
14013 consumer's partition on the cluster-default arm",
14014 );
14015 }
14016 }
14017
14018 #[test]
14019 fn declared_mesh_slots_placement_arm_routes_through_accessor() {
14020 // Composition pin: [`Caixa::declared_mesh_slots`]'s
14021 // `:placement` presence-probe arm must key off
14022 // [`Caixa::placement`], not the raw `self.placement.is_some()`
14023 // field-probe. Structurally: a `Caixa { placement:
14024 // Some(Placement::default()), .. }` must still push
14025 // `M3_AUTHOR_KEY_PLACEMENT` onto the declared-slot list (the
14026 // presence bit is `Some`, so the M3 kind-coherence gate must
14027 // surface the slot as "declared" even when every per-axis
14028 // scalar defers to the cluster-default arm), and a `Caixa {
14029 // placement: None, .. }` must NOT push the label (the "author
14030 // omitted the slot entirely" partition). The pair jointly pins
14031 // the accessor + declared-slot enumerator composition: any
14032 // future silent detour that had the accessor collapse
14033 // `Some(Placement::default())` to `None` (a `.filter(|p|
14034 // p.clusters().is_empty().not())` projection) would silently
14035 // absorb the "declared but empty" arm at the accessor boundary
14036 // and the [`crate::LayoutError::MeshSlotsOnNonAplicacao`]
14037 // kind-coherence gate would silently accept a struct-literal
14038 // `Caixa` carrying the drift.
14039 //
14040 // Peer of the sibling
14041 // `declared_servico_slots_limits_arm_routes_through_accessor`
14042 // (b2bd9d7),
14043 // `declared_servico_slots_behavior_arm_routes_through_accessor`
14044 // (35d8b52), and
14045 // `declared_mesh_slots_politicas_arm_routes_through_accessor`
14046 // (5d23d29) composition pins on the sibling `:limits` /
14047 // `:behavior` / `:politicas` outer-`Option<&Composite>` arms
14048 // — same "the enumerator gate must route through the
14049 // substrate-primitive typed dispatch" discipline extended onto
14050 // the second of the three M3 mesh-slot axes so the
14051 // [`Caixa::declared_mesh_slots`] enumerator carries the same
14052 // routing invariant on the `:placement` arm as the peer
14053 // `:politicas` arm.
14054 use crate::aplicacao::Placement;
14055 let c = caixa_aplicacao_with_placement(Some(Placement::default()));
14056 let slots = c.declared_mesh_slots();
14057 assert!(
14058 slots.contains(&crate::render::M3_AUTHOR_KEY_PLACEMENT),
14059 "declared_mesh_slots must push M3_AUTHOR_KEY_PLACEMENT \
14060 when `:placement` is Some (even for Placement::default()) \
14061 — the accessor and the enumerator gate must route through \
14062 the same substrate-primitive typed dispatch on the outer \
14063 :placement presence bit (got slots={slots:?})",
14064 );
14065 let c = caixa_aplicacao_with_placement(None);
14066 let slots = c.declared_mesh_slots();
14067 assert!(
14068 !slots.contains(&crate::render::M3_AUTHOR_KEY_PLACEMENT),
14069 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_PLACEMENT \
14070 when `:placement` is None — the author-omitted arm must \
14071 route through the accessor's None-return unchanged (got \
14072 slots={slots:?})",
14073 );
14074 }
14075
14076 #[test]
14077 fn aplicacao_view_placement_arm_folds_through_accessor() {
14078 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:placement`
14079 // Aplicacao-composition seed must fold through
14080 // [`Caixa::placement`], not the raw
14081 // `self.placement.clone().unwrap_or_default()` field-borrow.
14082 // Structurally: a `Caixa { placement: Some(Placement {
14083 // estrategia: Replicated, clusters: ["rio"], .. default }),
14084 // kind: Aplicacao, .. }` must surface a projected
14085 // [`crate::AplicacaoSpec`] whose `placement().estrategia()` +
14086 // `placement().clusters()` byte-equal the outer composite's
14087 // authored values (the fold must project the authored
14088 // composite verbatim), a `Caixa { placement:
14089 // Some(Placement::default()), kind: Aplicacao, .. }` must
14090 // surface an [`crate::AplicacaoSpec`] whose `placement()`
14091 // byte-equals [`crate::aplicacao::Placement::default`] (the
14092 // fold's empty-composite arm collapses to the same default
14093 // the author-omitted arm does), and a `Caixa { placement:
14094 // None, kind: Aplicacao, .. }` must surface an
14095 // [`crate::AplicacaoSpec`] whose `placement()` byte-equals
14096 // [`crate::aplicacao::Placement::default`] (the "author
14097 // omitted the slot entirely" arm folds through the
14098 // `unwrap_or_default` onto the cluster-default). The triad
14099 // jointly pins the accessor + Aplicacao-composition seed
14100 // composition: any future silent detour that had the accessor
14101 // divert the raw slot away from the seed's fold (an operator-
14102 // resolved overlay's default-fold arm silently differing from
14103 // the raw slot's default-fold arm) would silently split the
14104 // build-time distribution-artifact emission gate from the
14105 // caixa-mesh renderer's Aplicacao-view input at the
14106 // composition boundary.
14107 use crate::aplicacao::{Placement, PlacementStrategy};
14108 let c = caixa_aplicacao_with_placement(Some(Placement {
14109 estrategia: PlacementStrategy::Replicated,
14110 clusters: vec!["rio".into()],
14111 affinity: None,
14112 shard_key: None,
14113 }));
14114 let view = c.aplicacao_view().unwrap();
14115 assert_eq!(
14116 view.placement().estrategia(),
14117 PlacementStrategy::Replicated,
14118 "Caixa::aplicacao_view must fold the authored :placement \
14119 :estrategia scalar through the accessor verbatim onto the \
14120 projected AplicacaoSpec — a future silent detour at the \
14121 seed's fold arm would surface here as a projected-scalar \
14122 drift (got {:?})",
14123 view.placement().estrategia(),
14124 );
14125 assert_eq!(
14126 view.placement().clusters(),
14127 &["rio"],
14128 "Caixa::aplicacao_view must fold the authored :placement \
14129 :clusters list through the accessor verbatim onto the \
14130 projected AplicacaoSpec — a future silent detour at the \
14131 seed's fold arm would surface here as a projected-list \
14132 drift (got {:?})",
14133 view.placement().clusters(),
14134 );
14135 let c = caixa_aplicacao_with_placement(Some(Placement::default()));
14136 let view = c.aplicacao_view().unwrap();
14137 assert_eq!(
14138 view.placement(),
14139 &Placement::default(),
14140 "Caixa::aplicacao_view must fold Some(Placement::default()) \
14141 through the accessor onto Placement::default — the empty- \
14142 composite arm collapses to the same default the author- \
14143 omitted arm does (got {:?})",
14144 view.placement(),
14145 );
14146 let c = caixa_aplicacao_with_placement(None);
14147 let view = c.aplicacao_view().unwrap();
14148 assert_eq!(
14149 view.placement(),
14150 &Placement::default(),
14151 "Caixa::aplicacao_view must fold None through the accessor's \
14152 unwrap_or_default onto Placement::default — the author- \
14153 omitted arm must route through the accessor's None-return \
14154 unchanged (got {:?})",
14155 view.placement(),
14156 );
14157 }
14158
14159 #[test]
14160 fn placement_projects_option_ref_by_borrow() {
14161 // The by-borrow pin: [`Caixa::placement`] returns
14162 // `Option<&Placement>` by borrow — the returned reference
14163 // borrows the underlying `Option<Placement>` storage of the
14164 // `:placement` slot and the accessor must not clone the
14165 // backing composite on every call. Peer of the sibling
14166 // `limits_projects_option_ref_by_borrow` (b2bd9d7),
14167 // `behavior_projects_option_ref_by_borrow` (35d8b52), and
14168 // `politicas_projects_option_ref_by_borrow` (5d23d29) by-borrow
14169 // pins on the outer top-level [`Caixa`]
14170 // `Option<&Composite>`-return sub-family — extended here to
14171 // the fourth axis of the same sub-family: the accessor's
14172 // returned reference must borrow from `&self` (the returned
14173 // reference's lifetime is tied to `&self`), and calling the
14174 // accessor twice on the same [`Caixa`] must yield references
14175 // that are pointer-equal (the underlying byte-buffer is the
14176 // storage `Placement`'s allocation, not a fresh copy) as well
14177 // as value-equal (idempotent, no side effects on `&self`).
14178 //
14179 // Pins against a future silent detour that returned an owned
14180 // `Placement` (which would type-check via the `Clone` impl
14181 // but silently clone on every call), a `&Placement` panic-
14182 // return on the `None` arm (which would collapse the load-
14183 // bearing `Option` presence-bit into a runtime panic), or a
14184 // one-arm-only accessor that returned a saturating composite
14185 // on some sentinel input.
14186 use crate::aplicacao::{Placement, PlacementStrategy};
14187 for placement in [
14188 Some(Placement::default()),
14189 Some(Placement {
14190 estrategia: PlacementStrategy::Sharded,
14191 clusters: vec!["rio".into(), "sao-paulo".into()],
14192 affinity: Some("data-locality".into()),
14193 shard_key: Some("$tenantId".into()),
14194 }),
14195 ] {
14196 let c = caixa_aplicacao_with_placement(placement.clone());
14197 let first = c.placement().unwrap();
14198 let second = c.placement().unwrap();
14199 assert_eq!(
14200 first, second,
14201 "Caixa::placement must be idempotent — two successive \
14202 calls on the same &self must return the same \
14203 &Placement",
14204 );
14205 assert!(
14206 std::ptr::eq(first, second),
14207 "Caixa::placement must borrow the underlying \
14208 Option<Placement> storage — two successive calls \
14209 must return references with the same backing pointer \
14210 (a fresh Placement clone would change the pointer on \
14211 every call)",
14212 );
14213 assert_eq!(
14214 Some(first),
14215 placement.as_ref(),
14216 "Caixa::placement must return :placement verbatim by \
14217 borrow — got {first:?}, expected {:?}",
14218 placement.as_ref(),
14219 );
14220 }
14221 let c = caixa_aplicacao_with_placement(None);
14222 assert!(
14223 c.placement().is_none(),
14224 "Caixa::placement must return None when :placement is \
14225 absent — the author-omitted arm must project through the \
14226 accessor's Option::None unchanged",
14227 );
14228 }
14229
14230 // ── Caixa::entrada — outer top-level Option<&Entrada> composite-reference accessor ──
14231
14232 fn caixa_aplicacao_with_entrada(entrada: Option<crate::aplicacao::Entrada>) -> Caixa {
14233 use crate::aplicacao::{Membro, WitContract};
14234 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
14235 c.kind = CaixaKind::Aplicacao;
14236 c.membros = vec![Membro {
14237 caixa: "a".into(),
14238 versao: "^0.1".into(),
14239 }];
14240 c.contratos = vec![WitContract {
14241 de: "a".into(),
14242 para: "a".into(),
14243 wit: "wasi:http/proxy".into(),
14244 endpoint: Some("/x".into()),
14245 subject: None,
14246 slot: None,
14247 }];
14248 c.entrada = entrada;
14249 c
14250 }
14251
14252 #[test]
14253 fn entrada_returns_entrada_option_ref_verbatim_across_permutations() {
14254 // The canonical per-`Caixa` `:entrada` M3 mesh-slot outer-
14255 // composite optional-composite-reference-shape pin:
14256 // [`Caixa::entrada`] must return the `:entrada` typed
14257 // `Option<Entrada>` verbatim as an `Option<&Entrada>`
14258 // reference over the same backing storage the raw
14259 // `self.entrada.as_ref()` field access borrows from,
14260 // byte-equal across every representative fixture in the
14261 // accept-set — the author-omitted `None` shape (the
14262 // "cluster-internal Aplicacao" partition every downstream
14263 // Gateway-API emitter treats as "emit no listener + no
14264 // HTTPRoute"), a bare-`host`/`para` minimum-composite fixture
14265 // (empty `paths` — the resolved-paths fallback the peer
14266 // [`crate::aplicacao::Entrada::resolved_paths`] cascade folds
14267 // onto the substrate catch-all), and a fully-populated
14268 // multi-path-with-non-default-port fixture (the canonical
14269 // shape a public HTTP Aplicacao carries).
14270 //
14271 // Pins against a future silent detour that returned a fresh-
14272 // cloned [`crate::aplicacao::Entrada`] copy (which would
14273 // type-check via the `Clone` impl but silently break every
14274 // downstream caller that relied on the reference sharing the
14275 // composite's backing identity), a reference to an operator-
14276 // resolved overlay (the future per-cluster
14277 // `:entrada-overrides` slot — its resolution must land at
14278 // exactly this accessor body, not silently divert the raw
14279 // slot away from the peer [`Caixa::declared_mesh_slots`]
14280 // enumerator's presence probe), or an axis-shuffled projection
14281 // (a future detour that swapped `host` and `para` through the
14282 // accessor would silently split the paired
14283 // [`Caixa::aplicacao_view`] seed's forward input from the
14284 // sibling M3 gateway-artifact emitter's projection input).
14285 //
14286 // Fifth and final outer top-level [`Caixa`]
14287 // `Option<&Composite>`-return composite-reference accessor pin
14288 // on the substrate primitive — peer of the sibling
14289 // `limits_returns_limits_option_ref_verbatim_across_permutations`
14290 // (b2bd9d7),
14291 // `behavior_returns_behavior_option_ref_verbatim_across_permutations`
14292 // (35d8b52),
14293 // `politicas_returns_politicas_option_ref_verbatim_across_permutations`
14294 // (5d23d29), and
14295 // `placement_returns_placement_option_ref_verbatim_across_permutations`
14296 // (4fb8074) opening tetrad pins on the outer top-level
14297 // [`Caixa`] `Option<&Composite>`-return sub-family — extended
14298 // here to the third and final M3 mesh-slot axis so the closed
14299 // outer `Option<&Composite>` sub-family carries the same
14300 // "byte-equal, borrow-shared, presence-bit-preserved" outer-
14301 // accessor discipline across all five arms.
14302 use crate::aplicacao::Entrada;
14303 let fixtures: Vec<Option<Entrada>> = vec![
14304 None,
14305 Some(Entrada {
14306 host: "checkout.quero.cloud".into(),
14307 para: "gateway".into(),
14308 paths: Vec::new(),
14309 port: crate::DEFAULT_SERVICO_PORT,
14310 }),
14311 Some(Entrada {
14312 host: "api.pleme.io".into(),
14313 para: "public-api".into(),
14314 paths: vec!["/v1".into(), "/v2".into()],
14315 port: 8080,
14316 }),
14317 ];
14318 for entrada in fixtures {
14319 let c = caixa_aplicacao_with_entrada(entrada.clone());
14320 assert_eq!(
14321 c.entrada(),
14322 entrada.as_ref(),
14323 "Caixa::entrada must return :entrada verbatim (got \
14324 {:?}, expected {:?})",
14325 c.entrada(),
14326 entrada.as_ref(),
14327 );
14328 match (c.entrada(), c.entrada.as_ref()) {
14329 (Some(a), Some(b)) => assert!(
14330 std::ptr::eq(a, b),
14331 "Caixa::entrada accessor and self.entrada.as_ref() \
14332 field access must borrow the same backing storage \
14333 — the accessor is the substrate-primitive typed \
14334 dispatch every downstream Aplicacao-external- \
14335 gateway composite consumer must route through, and \
14336 a reference-identity split would silently break \
14337 every consumer that relied on the borrow sharing \
14338 the composite's storage",
14339 ),
14340 (None, None) => {}
14341 _ => panic!(
14342 "Caixa::entrada presence bit must byte-equal \
14343 self.entrada.is_some() — a presence-bit drift \
14344 would silently split the paired \
14345 Caixa::aplicacao_view Aplicacao-composition seed's \
14346 traversal head from the peer \
14347 Caixa::declared_mesh_slots M3 declared-slot \
14348 enumerator's presence probe",
14349 ),
14350 }
14351 assert_eq!(
14352 c.entrada().is_some(),
14353 c.entrada.is_some(),
14354 "Caixa::entrada().is_some() must byte-equal \
14355 self.entrada.is_some() — a presence-bit drift would \
14356 silently split every downstream Option<&Entrada> \
14357 consumer's partition on the cluster-internal arm",
14358 );
14359 }
14360 }
14361
14362 #[test]
14363 fn declared_mesh_slots_entrada_arm_routes_through_accessor() {
14364 // Composition pin: [`Caixa::declared_mesh_slots`]'s `:entrada`
14365 // presence-probe arm must key off [`Caixa::entrada`], not the
14366 // raw `self.entrada.is_some()` field-probe. Structurally: a
14367 // `Caixa { entrada: Some(Entrada { host: "...", para: "...",
14368 // paths: [], port: DEFAULT_SERVICO_PORT }), .. }` must push
14369 // `M3_AUTHOR_KEY_ENTRADA` onto the declared-slot list (the
14370 // presence bit is `Some`, so the M3 kind-coherence gate must
14371 // surface the slot as "declared" even when every per-axis
14372 // scalar defers to the substrate catch-all / default port),
14373 // and a `Caixa { entrada: None, .. }` must NOT push the label
14374 // (the "author omitted the slot entirely" partition). The pair
14375 // jointly pins the accessor + declared-slot enumerator
14376 // composition: any future silent detour that had the accessor
14377 // collapse `Some(Entrada { paths: [], .. })` to `None` (a
14378 // `.filter(|e| !e.paths.is_empty())` projection) would silently
14379 // absorb the "declared but empty-paths" arm at the accessor
14380 // boundary and the
14381 // [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
14382 // coherence gate would silently accept a struct-literal
14383 // `Caixa` carrying the drift.
14384 //
14385 // Peer of the sibling
14386 // `declared_servico_slots_limits_arm_routes_through_accessor`
14387 // (b2bd9d7),
14388 // `declared_servico_slots_behavior_arm_routes_through_accessor`
14389 // (35d8b52),
14390 // `declared_mesh_slots_politicas_arm_routes_through_accessor`
14391 // (5d23d29), and
14392 // `declared_mesh_slots_placement_arm_routes_through_accessor`
14393 // (4fb8074) composition pins on the sibling `:limits` /
14394 // `:behavior` / `:politicas` / `:placement` outer-
14395 // `Option<&Composite>` arms — same "the enumerator gate must
14396 // route through the substrate-primitive typed dispatch"
14397 // discipline extended onto the third and final M3 mesh-slot
14398 // axis so the [`Caixa::declared_mesh_slots`] enumerator now
14399 // carries the routing invariant on every M3 mesh-slot arm.
14400 use crate::aplicacao::Entrada;
14401 let c = caixa_aplicacao_with_entrada(Some(Entrada {
14402 host: "checkout.quero.cloud".into(),
14403 para: "gateway".into(),
14404 paths: Vec::new(),
14405 port: crate::DEFAULT_SERVICO_PORT,
14406 }));
14407 let slots = c.declared_mesh_slots();
14408 assert!(
14409 slots.contains(&crate::render::M3_AUTHOR_KEY_ENTRADA),
14410 "declared_mesh_slots must push M3_AUTHOR_KEY_ENTRADA when \
14411 `:entrada` is Some (even for empty-paths / default-port) \
14412 — the accessor and the enumerator gate must route through \
14413 the same substrate-primitive typed dispatch on the outer \
14414 :entrada presence bit (got slots={slots:?})",
14415 );
14416 let c = caixa_aplicacao_with_entrada(None);
14417 let slots = c.declared_mesh_slots();
14418 assert!(
14419 !slots.contains(&crate::render::M3_AUTHOR_KEY_ENTRADA),
14420 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_ENTRADA \
14421 when `:entrada` is None — the author-omitted arm must \
14422 route through the accessor's None-return unchanged (got \
14423 slots={slots:?})",
14424 );
14425 }
14426
14427 #[test]
14428 fn aplicacao_view_entrada_arm_folds_through_accessor() {
14429 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:entrada`
14430 // Aplicacao-composition seed must fold through
14431 // [`Caixa::entrada`], not the raw `self.entrada.clone()` field-
14432 // borrow. Structurally: a `Caixa { entrada: Some(Entrada {
14433 // host: "api.pleme.io", para: "public-api", paths: ["/v1"],
14434 // port: 8080 }), kind: Aplicacao, .. }` must surface a projected
14435 // [`crate::AplicacaoSpec`] whose `entrada().unwrap()` byte-
14436 // equals the outer composite's authored value (the fold must
14437 // project the authored composite verbatim), and a `Caixa {
14438 // entrada: None, kind: Aplicacao, .. }` must surface an
14439 // [`crate::AplicacaoSpec`] whose `entrada()` is `None` (the
14440 // "author omitted the slot entirely" arm folds through the
14441 // accessor's `Option::cloned` onto the same `None` presence
14442 // bit — unlike the peer `:politicas` / `:placement` arms
14443 // `:entrada` has no cluster-default fold, the omitted arm
14444 // stays omitted). The pair jointly pins the accessor +
14445 // Aplicacao-composition seed composition: any future silent
14446 // detour that had the accessor divert the raw slot away from
14447 // the seed's fold (an operator-resolved overlay's forward arm
14448 // silently differing from the raw slot's forward arm) would
14449 // silently split the build-time gateway-artifact emission gate
14450 // from the caixa-mesh renderer's Aplicacao-view input at the
14451 // composition boundary.
14452 use crate::aplicacao::Entrada;
14453 let authored = Entrada {
14454 host: "api.pleme.io".into(),
14455 para: "public-api".into(),
14456 paths: vec!["/v1".into()],
14457 port: 8080,
14458 };
14459 let c = caixa_aplicacao_with_entrada(Some(authored.clone()));
14460 let view = c.aplicacao_view().unwrap();
14461 assert_eq!(
14462 view.entrada(),
14463 Some(&authored),
14464 "Caixa::aplicacao_view must fold the authored :entrada \
14465 composite through the accessor verbatim onto the \
14466 projected AplicacaoSpec — a future silent detour at the \
14467 seed's fold arm would surface here as a projected- \
14468 composite drift (got {:?})",
14469 view.entrada(),
14470 );
14471 let c = caixa_aplicacao_with_entrada(None);
14472 let view = c.aplicacao_view().unwrap();
14473 assert!(
14474 view.entrada().is_none(),
14475 "Caixa::aplicacao_view must fold None through the \
14476 accessor's Option::cloned onto None — the author- \
14477 omitted arm must route through the accessor's None-return \
14478 unchanged (got {:?})",
14479 view.entrada(),
14480 );
14481 }
14482
14483 #[test]
14484 fn entrada_projects_option_ref_by_borrow() {
14485 // The by-borrow pin: [`Caixa::entrada`] returns
14486 // `Option<&Entrada>` by borrow — the returned reference
14487 // borrows the underlying `Option<Entrada>` storage of the
14488 // `:entrada` slot and the accessor must not clone the backing
14489 // composite on every call. Peer of the sibling
14490 // `limits_projects_option_ref_by_borrow` (b2bd9d7),
14491 // `behavior_projects_option_ref_by_borrow` (35d8b52),
14492 // `politicas_projects_option_ref_by_borrow` (5d23d29), and
14493 // `placement_projects_option_ref_by_borrow` (4fb8074) by-
14494 // borrow pins on the outer top-level [`Caixa`]
14495 // `Option<&Composite>`-return sub-family — extended here to
14496 // the fifth and final axis of the same sub-family, closing
14497 // the discipline: the accessor's returned reference must
14498 // borrow from `&self` (the returned reference's lifetime is
14499 // tied to `&self`), and calling the accessor twice on the
14500 // same [`Caixa`] must yield references that are pointer-equal
14501 // (the underlying byte-buffer is the storage `Entrada`'s
14502 // allocation, not a fresh copy) as well as value-equal
14503 // (idempotent, no side effects on `&self`).
14504 //
14505 // Pins against a future silent detour that returned an owned
14506 // `Entrada` (which would type-check via the `Clone` impl but
14507 // silently clone on every call), a `&Entrada` panic-return on
14508 // the `None` arm (which would collapse the load-bearing
14509 // `Option` presence-bit into a runtime panic), or a one-arm-
14510 // only accessor that returned a saturating composite on some
14511 // sentinel input.
14512 use crate::aplicacao::Entrada;
14513 for entrada in [
14514 Some(Entrada {
14515 host: "checkout.quero.cloud".into(),
14516 para: "gateway".into(),
14517 paths: Vec::new(),
14518 port: crate::DEFAULT_SERVICO_PORT,
14519 }),
14520 Some(Entrada {
14521 host: "api.pleme.io".into(),
14522 para: "public-api".into(),
14523 paths: vec!["/v1".into(), "/v2".into()],
14524 port: 8080,
14525 }),
14526 ] {
14527 let c = caixa_aplicacao_with_entrada(entrada.clone());
14528 let first = c.entrada().unwrap();
14529 let second = c.entrada().unwrap();
14530 assert_eq!(
14531 first, second,
14532 "Caixa::entrada must be idempotent — two successive \
14533 calls on the same &self must return the same &Entrada",
14534 );
14535 assert!(
14536 std::ptr::eq(first, second),
14537 "Caixa::entrada must borrow the underlying \
14538 Option<Entrada> storage — two successive calls must \
14539 return references with the same backing pointer (a \
14540 fresh Entrada clone would change the pointer on every \
14541 call)",
14542 );
14543 assert_eq!(
14544 Some(first),
14545 entrada.as_ref(),
14546 "Caixa::entrada must return :entrada verbatim by \
14547 borrow — got {first:?}, expected {:?}",
14548 entrada.as_ref(),
14549 );
14550 }
14551 let c = caixa_aplicacao_with_entrada(None);
14552 assert!(
14553 c.entrada().is_none(),
14554 "Caixa::entrada must return None when :entrada is absent \
14555 — the author-omitted arm must project through the \
14556 accessor's Option::None unchanged",
14557 );
14558 }
14559
14560 // ── Caixa::estrategia — outer top-level Option<RestartStrategy> flat-spread supervisor-tree accessor ──
14561
14562 fn caixa_with_estrategia(estrategia: Option<crate::supervisor::RestartStrategy>) -> Caixa {
14563 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
14564 c.estrategia = estrategia;
14565 c
14566 }
14567
14568 #[test]
14569 fn estrategia_returns_estrategia_option_verbatim_across_permutations() {
14570 // The canonical per-`Caixa` `:estrategia` M2 supervisor-tree-slot
14571 // flat-spread `Option<RestartStrategy>`-return `Copy`-composite-
14572 // enum-arm scalar shape pin: [`Caixa::estrategia`] must return
14573 // the `:estrategia` typed `Option<crate::supervisor::RestartStrategy>`
14574 // verbatim as an `Option<RestartStrategy>` `Copy`-projected value
14575 // over the same discriminant the raw `self.estrategia` field
14576 // access carries, byte-equal across every representative fixture
14577 // in the accept-set — the author-omitted `None` shape (the
14578 // "defer to [`RestartStrategy::default`] through the
14579 // [`Self::supervisor_view`] `unwrap_or_default()` fold" partition
14580 // every non-`Supervisor`-kind `defcaixa` carries by
14581 // `#[serde(default)]`), and each of the four closed-set variants
14582 // [`RestartStrategy::OneForOne`] / [`RestartStrategy::OneForAll`]
14583 // / [`RestartStrategy::RestForOne`] /
14584 // [`RestartStrategy::SimpleOneForOne`] the author-declared arm
14585 // partitions on.
14586 //
14587 // Pins against a future silent detour that re-derived the
14588 // strategy from a peer axis (an accidental fallback to
14589 // `if children.is_empty() { SimpleOneForOne } else { OneForOne }`
14590 // collapse that read the outer `:children` list-length axis into
14591 // the strategy discriminator at the accessor boundary), a
14592 // stale-derive detour that substituted [`RestartStrategy::default`]
14593 // when the outer `Option` held `None` (which would silently
14594 // collapse the load-bearing "author explicitly declared
14595 // `:estrategia OneForOne`" vs "author omitted the slot and
14596 // inherited the default" partition the [`Self::declared_supervisor_slots`]
14597 // presence-probe reads — the enumerator gate would still push
14598 // `SUPERVISOR_AUTHOR_KEY_ESTRATEGIA` on the omitted arm, silently
14599 // splitting the paired [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
14600 // kind-coherence gate's traversal head from the
14601 // [`Self::supervisor_view`] `unwrap_or_default()` fold's
14602 // composition head), a reference to an operator-resolved overlay
14603 // (the future per-cluster `:estrategia-overrides` slot — its
14604 // resolution must land at exactly this accessor body, not
14605 // silently divert the raw slot away from a second consumer), or
14606 // an axis-remap projection (a future detour that mapped
14607 // `OneForAll` through the accessor onto `OneForOne` would
14608 // silently split every downstream sibling-restart-strategy
14609 // consumer's per-arm fan-out).
14610 //
14611 // First outer top-level [`Caixa`] `Option<Copy>`-return
14612 // supervisor-tree-slot flat-spread accessor pin on the substrate
14613 // primitive — opens the outer-`Caixa` `Option<Copy>` flat-spread
14614 // projection pattern the sibling per-`Caixa` `:max-restarts` /
14615 // `:restart-window` future outer-scalar pins fold on. Peer of
14616 // the inner-altitude
14617 // `supervisor_spec_estrategia_returns_estrategia_verbatim_across_permutations`
14618 // (eafb619) pin on the post-composition [`SupervisorSpec`]
14619 // altitude — same "the substrate-primitive accessor must byte-
14620 // equal the raw field access verbatim across every author-
14621 // declared value" discipline extended onto the pre-composition
14622 // outer author-surface [`Caixa`] altitude. Peer of the closed
14623 // outer-`Caixa` `Option<&Composite>` composite-reference family
14624 // the sibling `limits` / `behavior` / `politicas` / `placement` /
14625 // `entrada`
14626 // `..._returns_..._option_ref_verbatim_across_permutations` pins
14627 // already carry on the outer `Option<&Composite>` altitude.
14628 use crate::supervisor::RestartStrategy;
14629 let fixtures: Vec<Option<RestartStrategy>> = vec![
14630 None,
14631 Some(RestartStrategy::OneForOne),
14632 Some(RestartStrategy::OneForAll),
14633 Some(RestartStrategy::RestForOne),
14634 Some(RestartStrategy::SimpleOneForOne),
14635 ];
14636 for estrategia in fixtures {
14637 let c = caixa_with_estrategia(estrategia);
14638 assert_eq!(
14639 c.estrategia(),
14640 estrategia,
14641 "Caixa::estrategia must return :estrategia verbatim (got \
14642 {:?}, expected {:?})",
14643 c.estrategia(),
14644 estrategia,
14645 );
14646 assert_eq!(
14647 c.estrategia(),
14648 c.estrategia,
14649 "Caixa::estrategia accessor and self.estrategia field \
14650 access must byte-equal — the accessor is the substrate-\
14651 primitive typed dispatch every downstream supervisor-\
14652 tree flat-spread consumer must route through, and a \
14653 discriminant split would silently break every consumer \
14654 that relied on the accessor sharing the field's own \
14655 Option<Copy> shape",
14656 );
14657 assert_eq!(
14658 c.estrategia().is_some(),
14659 c.estrategia.is_some(),
14660 "Caixa::estrategia().is_some() must byte-equal \
14661 self.estrategia.is_some() — a presence-bit drift would \
14662 silently split the paired Caixa::declared_supervisor_slots \
14663 presence-probe arm from the Caixa::supervisor_view \
14664 unwrap_or_default() fold's composition input",
14665 );
14666 }
14667 }
14668
14669 #[test]
14670 fn declared_supervisor_slots_estrategia_arm_routes_through_accessor() {
14671 // Composition pin: [`Caixa::declared_supervisor_slots`]'s
14672 // `:estrategia` presence-probe arm must key off
14673 // [`Caixa::estrategia`], not the raw `self.estrategia.is_some()`
14674 // field-probe. Structurally: every `Caixa { estrategia:
14675 // Some(RestartStrategy::_), .. }` variant must push
14676 // `SUPERVISOR_AUTHOR_KEY_ESTRATEGIA` onto the declared-slot list
14677 // (the presence bit is `Some` for every closed-set variant, so
14678 // the M2 supervisor-tree kind-coherence gate must surface the
14679 // slot as "declared" regardless of which variant the author
14680 // picked), and a `Caixa { estrategia: None, .. }` must NOT push
14681 // the label (the "author omitted the slot entirely, deferring
14682 // to [`RestartStrategy::default`] through the supervisor_view
14683 // fold" partition). The pair jointly pins the accessor +
14684 // declared-slot enumerator composition: any future silent detour
14685 // that had the accessor collapse `Some(RestartStrategy::default())`
14686 // to `None` (a `.filter(|e| *e != RestartStrategy::default())`
14687 // projection) would silently absorb the "declared but default-
14688 // valued" arm at the accessor boundary and the
14689 // [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] kind-
14690 // coherence gate would silently accept a struct-literal `Caixa`
14691 // carrying the drift.
14692 //
14693 // Peer of the sibling per-`Caixa`
14694 // `declared_servico_slots_limits_arm_routes_through_accessor`
14695 // (b2bd9d7) accessor-composition pin on the sibling outer-`Caixa`
14696 // `Option<&LimitsSpec>` composition axis — same "the enumerator
14697 // gate must route through the substrate-primitive typed
14698 // dispatch" discipline extended onto the flat-spread M2
14699 // supervisor-tree `Option<RestartStrategy>`-composition surface,
14700 // opening the outer-`Caixa` supervisor-tree-slot arm of the
14701 // composition-pin family.
14702 use crate::supervisor::RestartStrategy;
14703 for estrategia in [
14704 RestartStrategy::OneForOne,
14705 RestartStrategy::OneForAll,
14706 RestartStrategy::RestForOne,
14707 RestartStrategy::SimpleOneForOne,
14708 ] {
14709 let c = caixa_with_estrategia(Some(estrategia));
14710 let slots = c.declared_supervisor_slots();
14711 assert!(
14712 slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA),
14713 "declared_supervisor_slots must push \
14714 SUPERVISOR_AUTHOR_KEY_ESTRATEGIA when `:estrategia` is \
14715 Some({estrategia:?}) — the accessor and the enumerator \
14716 gate must route through the same substrate-primitive \
14717 typed dispatch on the outer :estrategia presence bit \
14718 (got slots={slots:?})",
14719 );
14720 }
14721 let c = caixa_with_estrategia(None);
14722 let slots = c.declared_supervisor_slots();
14723 assert!(
14724 !slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA),
14725 "declared_supervisor_slots must NOT push \
14726 SUPERVISOR_AUTHOR_KEY_ESTRATEGIA when `:estrategia` is None \
14727 — the author-omitted arm must route through the accessor's \
14728 None-return unchanged (got slots={slots:?})",
14729 );
14730 }
14731
14732 #[test]
14733 fn supervisor_view_estrategia_arm_routes_through_accessor() {
14734 // Composition pin: [`Caixa::supervisor_view`]'s per-`:estrategia`
14735 // [`SupervisorSpec`] construction arm must key off
14736 // [`Caixa::estrategia`]'s `unwrap_or_default()` fold, not the raw
14737 // `self.estrategia.unwrap_or_default()` field-fold. Structurally:
14738 // for every `:kind Supervisor` `Caixa` carrying an author-
14739 // declared `Some(RestartStrategy::_)` variant, the composed
14740 // [`SupervisorSpec`]'s `.estrategia` field must byte-equal the
14741 // outer accessor's declared variant unchanged; and for a
14742 // `:kind Supervisor` `Caixa` carrying `None`, the composed
14743 // [`SupervisorSpec`]'s `.estrategia` field must byte-equal
14744 // [`RestartStrategy::default`] (the [`RestartStrategy::OneForOne`]
14745 // arm the flat-spread `unwrap_or_default()` fold projects to on
14746 // the author-omitted arm — this is the *composition* between the
14747 // outer `Option<RestartStrategy>` accessor's presence-bit
14748 // surface and the inner post-composition non-`Option`
14749 // [`SupervisorSpec::estrategia`] altitude). The pair jointly
14750 // pins the accessor + supervisor_view composition: any future
14751 // silent detour that had the accessor promote `None` to
14752 // `Some(RestartStrategy::default())` (a `.or_else(|| Some(RestartStrategy::default()))`
14753 // projection) would silently collapse the two arms into one at
14754 // the accessor boundary and the [`Self::declared_supervisor_slots`]
14755 // presence probe would silently drift from the composition site.
14756 //
14757 // Peer of the sibling M2 supervisor-slot post-composition
14758 // `validate_reads_through_lifted_estrategia_accessor` (eafb619)
14759 // pin on the [`SupervisorSpec::validate`] altitude — this pin
14760 // extends that inner-altitude accessor-routing discipline onto
14761 // the pre-composition outer author-surface [`Caixa`] altitude,
14762 // pinning the composition edge between the flat-spread outer
14763 // `Option<RestartStrategy>` and the composed [`SupervisorSpec`]
14764 // `RestartStrategy` axes.
14765 use crate::CaixaKind;
14766 use crate::supervisor::{ChildSpec, RestartPolicy, RestartStrategy};
14767 for estrategia in [
14768 RestartStrategy::OneForOne,
14769 RestartStrategy::OneForAll,
14770 RestartStrategy::RestForOne,
14771 RestartStrategy::SimpleOneForOne,
14772 ] {
14773 let mut c = caixa_with_estrategia(Some(estrategia));
14774 c.kind = CaixaKind::Supervisor;
14775 // Route the `SimpleOneForOne ↔ non-SimpleOneForOne` fixture-
14776 // shape partition through the [`gen_platform::IsVariant`]
14777 // derive-generated
14778 // [`RestartStrategy::is_simple_one_for_one`] predicate rather
14779 // than the raw `matches!(estrategia, RestartStrategy::
14780 // SimpleOneForOne)` open-coded pattern-match — same closed-
14781 // set-typed-enum arm-discriminator dispatch discipline the
14782 // sibling [`crate::upgrade::UpgradeInstruction::is_restart`]
14783 // convergence (915a934) extended onto its two paired positive
14784 // / negated `matches!` sites and the peer
14785 // [`crate::aplicacao::PlacementStrategy`] `IsVariant`-derived
14786 // predicate convergence (766ec63) extended onto the M3 mesh-
14787 // slot per-`:placement` distribution-strategy discriminator
14788 // axis. See the sibling `supervisor::tests::
14789 // round_trip_all_strategies` and
14790 // `supervisor::tests::supervisor_spec_estrategia_returns_estrategia_verbatim_across_permutations`
14791 // fixtures — the three sites (all test-only,
14792 // acknowledged in 915a934's Prior-commits footnote as the
14793 // outstanding follow-up) now consult one typed dispatch on
14794 // the substrate primitive.
14795 c.children = if estrategia.is_simple_one_for_one() {
14796 Vec::new()
14797 } else {
14798 vec![ChildSpec {
14799 caixa: "worker".into(),
14800 versao: "^0.1".into(),
14801 restart: RestartPolicy::Permanent,
14802 }]
14803 };
14804 let view = c.supervisor_view().expect(
14805 "supervisor_view must materialize a SupervisorSpec for a \
14806 :kind Supervisor Caixa carrying a Some(:estrategia) slot",
14807 );
14808 assert_eq!(
14809 view.estrategia(),
14810 c.estrategia().unwrap(),
14811 "supervisor_view must carry the outer Caixa::estrategia() \
14812 declared variant onto the composed SupervisorSpec.estrategia \
14813 field verbatim on the Some arm (got {:?}, expected {:?})",
14814 view.estrategia(),
14815 c.estrategia().unwrap(),
14816 );
14817 }
14818 // The author-omitted arm: outer `None` → composed
14819 // `RestartStrategy::default()` through the flat-spread
14820 // `unwrap_or_default()` fold.
14821 let mut c = caixa_with_estrategia(None);
14822 c.kind = CaixaKind::Supervisor;
14823 // Populate children so the sibling supervisor slots are coherent
14824 // for the [`Self::supervisor_view`] projection; the `:estrategia`
14825 // arm still defers to [`RestartStrategy::default`] on the
14826 // author-omitted arm even when the sibling slots carry values.
14827 c.children = vec![ChildSpec {
14828 caixa: "worker".into(),
14829 versao: "^0.1".into(),
14830 restart: RestartPolicy::Permanent,
14831 }];
14832 let view = c.supervisor_view().expect(
14833 "supervisor_view must materialize a SupervisorSpec for a \
14834 :kind Supervisor Caixa carrying a None `:estrategia` slot",
14835 );
14836 assert_eq!(
14837 view.estrategia(),
14838 RestartStrategy::default(),
14839 "supervisor_view must project the outer Caixa::estrategia() \
14840 None arm onto RestartStrategy::default() through the flat-\
14841 spread unwrap_or_default() fold (got {:?}, expected {:?})",
14842 view.estrategia(),
14843 RestartStrategy::default(),
14844 );
14845 assert!(
14846 c.estrategia().is_none(),
14847 "Caixa::estrategia() must remain None on the author-omitted \
14848 arm — the supervisor_view fold must not mutate the outer \
14849 flat-spread presence bit",
14850 );
14851 }
14852
14853 #[test]
14854 fn estrategia_projects_option_by_copy() {
14855 // The by-`Copy` pin: [`Caixa::estrategia`] returns
14856 // `Option<RestartStrategy>` by value (`RestartStrategy: Copy`) —
14857 // the accessor does not borrow `&self` past the call (no
14858 // lifetime on the return type), and calling the accessor twice
14859 // on the same [`Caixa`] must yield discriminant-equal values
14860 // (idempotent, no side effects on `&self`). Peer of the sibling
14861 // outer-`Caixa` `Option<&Composite>` by-borrow
14862 // `limits_projects_option_ref_by_borrow` (b2bd9d7) /
14863 // `behavior_projects_option_ref_by_borrow` (35d8b52) /
14864 // `politicas_projects_option_ref_by_borrow` (5d23d29) /
14865 // `placement_projects_option_ref_by_borrow` (4fb8074) /
14866 // `entrada_projects_option_ref_by_borrow` (e4128e4) by-borrow
14867 // pins on the outer-`Caixa` `Option<&Composite>`-return axes —
14868 // extended here to the outer-`Caixa` `Option<Copy>`-return
14869 // flat-spread axis. The `Copy` discipline replaces the pointer-
14870 // equality claim the by-borrow siblings pin (a fresh `Copy` of a
14871 // `Copy` discriminant is definitionally the same discriminant, so
14872 // the axis reduces to discriminant equality).
14873 //
14874 // Pins against a future silent detour that returned a fresh
14875 // `Option<&RestartStrategy>` (which would type-check but silently
14876 // introduce a borrow of `&self` past the call, collapsing the
14877 // load-bearing "no lifetime on the return type" `Copy` projection
14878 // the flat-spread axis's `Option<Copy>` shape carries), a stale-
14879 // read side effect that flipped the outer discriminant on
14880 // successive calls, or an axis-remap projection that returned a
14881 // different variant than the field storage.
14882 use crate::supervisor::RestartStrategy;
14883 for estrategia in [
14884 Some(RestartStrategy::OneForOne),
14885 Some(RestartStrategy::OneForAll),
14886 Some(RestartStrategy::RestForOne),
14887 Some(RestartStrategy::SimpleOneForOne),
14888 ] {
14889 let c = caixa_with_estrategia(estrategia);
14890 let first = c.estrategia();
14891 let second = c.estrategia();
14892 assert_eq!(
14893 first, second,
14894 "Caixa::estrategia must be idempotent — two successive \
14895 calls on the same &self must return the same \
14896 Option<RestartStrategy>",
14897 );
14898 assert_eq!(
14899 first, estrategia,
14900 "Caixa::estrategia must return :estrategia verbatim by \
14901 Copy — got {first:?}, expected {estrategia:?}",
14902 );
14903 }
14904 let c = caixa_with_estrategia(None);
14905 assert!(
14906 c.estrategia().is_none(),
14907 "Caixa::estrategia must return None when :estrategia is \
14908 absent — the author-omitted arm must project through the \
14909 accessor's Option::None unchanged",
14910 );
14911 }
14912
14913 // ── Caixa::max_restarts / Caixa::restart_window —
14914 // outer top-level M2 supervisor-tree-slot flat-spread accessors
14915 // (Option<u32> / Option<&str>) folding on the ed04d3c
14916 // Caixa::estrategia Option<Copy> sub-family ─────────────────────
14917
14918 fn caixa_with_max_restarts(max_restarts: Option<u32>) -> Caixa {
14919 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
14920 c.max_restarts = max_restarts;
14921 c
14922 }
14923
14924 fn caixa_supervisor_with_max_restarts_and_window(
14925 max_restarts: Option<u32>,
14926 restart_window: Option<&str>,
14927 ) -> Caixa {
14928 use crate::CaixaKind;
14929 use crate::supervisor::{ChildSpec, RestartPolicy};
14930 let mut c = Caixa::from_lisp(&Caixa::template("root")).unwrap();
14931 c.kind = CaixaKind::Supervisor;
14932 c.max_restarts = max_restarts;
14933 c.restart_window = restart_window.map(str::to_string);
14934 c.children = vec![ChildSpec {
14935 caixa: "worker".into(),
14936 versao: "^0.1".into(),
14937 restart: RestartPolicy::Permanent,
14938 }];
14939 c
14940 }
14941
14942 #[test]
14943 fn max_restarts_returns_max_restarts_option_verbatim_across_permutations() {
14944 // Value-shape pin: [`Caixa::max_restarts`] returns the
14945 // `:max-restarts` typed `Option<u32>` verbatim, `Copy`-projected
14946 // from the typed slot's own storage, byte-equal across the
14947 // author-omitted `None` arm (the "defer to the
14948 // [`Self::supervisor_view`] `unwrap_or(5)` OTP-canonical
14949 // `{intensity, 5, 60}` default" partition every
14950 // non-`Supervisor`-kind caixa carries by `#[serde(default)]`)
14951 // and each of the representative fixtures in the accept-set —
14952 // `0` (the zero-floor arm the peer
14953 // [`crate::supervisor::SupervisorSpec::validate`]
14954 // [`crate::SupervisorError::ZeroMaxRestarts`] gate refuses on
14955 // the post-composition altitude — the accessor must ship the
14956 // raw slot verbatim so struct-literal fixtures continue to
14957 // expose the zero at the accessor boundary), the OTP-canonical
14958 // `5` default (`{intensity, 5, 60}` worker-supervisor from
14959 // Learn You Some Erlang), `1000` (the
14960 // [`SUPERVISOR_MAX_RESTARTS_MAX`] cap the peer post-composition
14961 // upper-bound gate accepts on the boundary), `u32::MAX` (a
14962 // past-the-cap sentinel that the substrate-primitive accessor
14963 // must still ship verbatim). Second outer top-level
14964 // [`Caixa`] `Option<Copy>`-return supervisor-tree flat-spread
14965 // pin — folds on the sibling
14966 // `estrategia_returns_estrategia_option_verbatim_across_permutations`
14967 // (ed04d3c) pin's `Option<Copy>` shape, extending the sub-family
14968 // onto the sibling `Option<u32>` restart-budget-count arm.
14969 let fixtures: Vec<Option<u32>> = vec![None, Some(0), Some(5), Some(1000), Some(u32::MAX)];
14970 for max_restarts in fixtures {
14971 let c = caixa_with_max_restarts(max_restarts);
14972 assert_eq!(
14973 c.max_restarts(),
14974 max_restarts,
14975 "Caixa::max_restarts must return :max-restarts verbatim \
14976 (got {:?}, expected {max_restarts:?})",
14977 c.max_restarts(),
14978 );
14979 assert_eq!(
14980 c.max_restarts(),
14981 c.max_restarts,
14982 "Caixa::max_restarts accessor and self.max_restarts \
14983 field access must byte-equal — a presence-bit or count \
14984 drift would silently split the paired \
14985 Caixa::declared_supervisor_slots presence-probe arm \
14986 from the Caixa::supervisor_view unwrap_or(5) fold's \
14987 composition input",
14988 );
14989 }
14990 }
14991
14992 #[test]
14993 fn max_restarts_projects_option_by_copy() {
14994 // The by-`Copy` pin: [`Caixa::max_restarts`] returns
14995 // `Option<u32>` by value (`u32: Copy`) — the accessor does not
14996 // borrow `&self` past the call (no lifetime on the return type),
14997 // and calling the accessor twice on the same [`Caixa`] must
14998 // yield equal values (idempotent, no side effects). Peer of the
14999 // sibling `estrategia_projects_option_by_copy` (ed04d3c) pin on
15000 // the outer-`Caixa` `Option<Copy>`-return flat-spread axis.
15001 for max_restarts in [Some(0u32), Some(5), Some(1000), Some(u32::MAX), None] {
15002 let c = caixa_with_max_restarts(max_restarts);
15003 let first = c.max_restarts();
15004 let second = c.max_restarts();
15005 assert_eq!(
15006 first, second,
15007 "Caixa::max_restarts must be idempotent — two successive \
15008 calls on the same &self must return the same Option<u32>",
15009 );
15010 assert_eq!(
15011 first, max_restarts,
15012 "Caixa::max_restarts must return :max-restarts verbatim \
15013 by Copy — got {first:?}, expected {max_restarts:?}",
15014 );
15015 }
15016 }
15017
15018 #[test]
15019 fn declared_supervisor_slots_max_restarts_arm_routes_through_accessor() {
15020 // Composition pin: [`Caixa::declared_supervisor_slots`]'s
15021 // `:max-restarts` presence-probe arm must key off
15022 // [`Caixa::max_restarts`], not the raw
15023 // `self.max_restarts.is_some()` field-probe. Structurally: every
15024 // `Caixa { max_restarts: Some(_), .. }` variant must push
15025 // `SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS` onto the declared-slot
15026 // list (the presence bit is `Some` for every representative
15027 // count, so the M2 kind-coherence gate must surface the slot as
15028 // "declared"), and a `Caixa { max_restarts: None, .. }` must
15029 // NOT push the label. Peer of the sibling
15030 // `declared_supervisor_slots_estrategia_arm_routes_through_accessor`
15031 // (ed04d3c) composition pin — same routing-through-accessor
15032 // discipline extended onto the sibling flat-spread `Option<u32>`
15033 // arm.
15034 for max_restarts in [0u32, 5, 1000, u32::MAX] {
15035 let c = caixa_with_max_restarts(Some(max_restarts));
15036 let slots = c.declared_supervisor_slots();
15037 assert!(
15038 slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS),
15039 "declared_supervisor_slots must push \
15040 SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS when `:max-restarts` \
15041 is Some({max_restarts}) — the accessor and the \
15042 enumerator gate must route through the same \
15043 substrate-primitive typed dispatch on the outer \
15044 :max-restarts presence bit (got slots={slots:?})",
15045 );
15046 }
15047 let c = caixa_with_max_restarts(None);
15048 let slots = c.declared_supervisor_slots();
15049 assert!(
15050 !slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS),
15051 "declared_supervisor_slots must NOT push \
15052 SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS when `:max-restarts` is \
15053 None — the author-omitted arm must route through the \
15054 accessor's None-return unchanged (got slots={slots:?})",
15055 );
15056 }
15057
15058 #[test]
15059 fn supervisor_view_max_restarts_arm_routes_through_accessor() {
15060 // Composition pin: [`Caixa::supervisor_view`]'s per-`:max-restarts`
15061 // [`SupervisorSpec`] construction arm must key off
15062 // [`Caixa::max_restarts`]'s `unwrap_or(5)` fold, not the raw
15063 // `self.max_restarts.unwrap_or(5)` field-fold. Structurally: for
15064 // every `:kind Supervisor` `Caixa` carrying an author-declared
15065 // `Some(n)`, the composed [`SupervisorSpec`]'s `.max_restarts()`
15066 // must byte-equal `n`; and for a `:kind Supervisor` `Caixa`
15067 // carrying `None`, the composed [`SupervisorSpec`]'s
15068 // `.max_restarts()` must byte-equal the OTP-canonical `5`. Peer
15069 // of the sibling
15070 // `supervisor_view_estrategia_arm_routes_through_accessor`
15071 // (ed04d3c) composition pin.
15072 for max_restarts in [1u32, 5, 1000] {
15073 let c = caixa_supervisor_with_max_restarts_and_window(Some(max_restarts), None);
15074 let view = c.supervisor_view().expect(
15075 "supervisor_view must materialize a SupervisorSpec for a \
15076 :kind Supervisor Caixa carrying a Some(:max-restarts)",
15077 );
15078 assert_eq!(
15079 view.max_restarts(),
15080 max_restarts,
15081 "supervisor_view must carry the outer \
15082 Caixa::max_restarts() Some arm onto the composed \
15083 SupervisorSpec.max_restarts field verbatim (got {}, \
15084 expected {max_restarts})",
15085 view.max_restarts(),
15086 );
15087 }
15088 let c = caixa_supervisor_with_max_restarts_and_window(None, None);
15089 let view = c.supervisor_view().expect(
15090 "supervisor_view must materialize a SupervisorSpec for a \
15091 :kind Supervisor Caixa carrying a None :max-restarts",
15092 );
15093 assert_eq!(
15094 view.max_restarts(),
15095 5,
15096 "supervisor_view must project the outer \
15097 Caixa::max_restarts() None arm onto the OTP-canonical \
15098 {{intensity, 5, 60}} default (5) through the flat-spread \
15099 unwrap_or(5) fold (got {})",
15100 view.max_restarts(),
15101 );
15102 assert!(
15103 c.max_restarts().is_none(),
15104 "Caixa::max_restarts() must remain None on the author-\
15105 omitted arm — the supervisor_view fold must not mutate \
15106 the outer flat-spread presence bit",
15107 );
15108 }
15109
15110 #[test]
15111 fn restart_window_returns_restart_window_option_verbatim_across_permutations() {
15112 // Value-shape pin: [`Caixa::restart_window`] returns the
15113 // `:restart-window` typed `Option<String>` verbatim as an
15114 // `Option<&str>`, borrowed from the typed slot's own storage,
15115 // byte-equal across the author-omitted `None` arm and each of
15116 // the representative fixtures in the accept-set — the canonical
15117 // `"60s"` from `{intensity, 5, 60}`, the sibling
15118 // canonical-magnitude forms (`"5m"` / `"1h"` / `"500ms"` / `"30"`
15119 // / `"0s"`) the shared codec's positive-set sweep pin covers,
15120 // plus a past-the-guard sentinel (`"1.5s"` — the fractional-
15121 // seconds drift the sibling [`Self::validate_restart_window`]
15122 // gate refuses; the accessor must ship the raw slot verbatim
15123 // so struct-literal fixtures continue to expose the drift at
15124 // the accessor boundary). Third outer top-level [`Caixa`]
15125 // supervisor-tree flat-spread pin — extends the sub-family onto
15126 // the sibling `Option<&str>` raw-duration-string arm.
15127 for window in [
15128 None,
15129 Some("60s"),
15130 Some("5m"),
15131 Some("1h"),
15132 Some("500ms"),
15133 Some("1.5s"),
15134 Some(""),
15135 ] {
15136 let c = caixa_with_restart_window(window);
15137 assert_eq!(
15138 c.restart_window(),
15139 window,
15140 "Caixa::restart_window must return :restart-window \
15141 verbatim as Option<&str> (got {:?}, expected {window:?})",
15142 c.restart_window(),
15143 );
15144 assert_eq!(
15145 c.restart_window(),
15146 c.restart_window.as_deref(),
15147 "Caixa::restart_window accessor and \
15148 self.restart_window.as_deref() field access must \
15149 byte-equal — a byte-level drift would silently split \
15150 the paired Caixa::declared_supervisor_slots \
15151 presence-probe arm from the \
15152 Caixa::validate_restart_window shared-codec gate and \
15153 the Caixa::supervisor_view soft-swallowing fold",
15154 );
15155 }
15156 }
15157
15158 #[test]
15159 fn restart_window_projects_slice_by_borrow() {
15160 // The by-borrow pin: [`Caixa::restart_window`] returns
15161 // `Option<&str>` by borrow — the returned string slice borrows
15162 // the underlying `Option<String>` storage of the `:restart-window`
15163 // slot and the accessor must not clone on every call. Peer of
15164 // the sibling outer top-level [`Caixa`] `Option<&str>`-return
15165 // by-borrow pins on the universal-axis scalar family
15166 // (`licenca_projects_option_ref_by_borrow` /
15167 // `descricao_projects_option_ref_by_borrow` and siblings) —
15168 // extended onto the M2 supervisor-tree flat-spread
15169 // `Option<&str>` raw-duration-string axis.
15170 for window in [None, Some("60s"), Some("5m"), Some("")] {
15171 let c = caixa_with_restart_window(window);
15172 let first = c.restart_window();
15173 let second = c.restart_window();
15174 assert_eq!(
15175 first, second,
15176 "Caixa::restart_window must be idempotent — two \
15177 successive calls on the same &self must return the \
15178 same Option<&str>",
15179 );
15180 if let (Some(a), Some(b)) = (first, second) {
15181 assert_eq!(
15182 a.as_ptr(),
15183 b.as_ptr(),
15184 "Caixa::restart_window must borrow the underlying \
15185 String storage — two successive Some-arm calls must \
15186 return slices with the same backing pointer (a fresh \
15187 String clone would change the pointer on every call)",
15188 );
15189 }
15190 assert_eq!(
15191 first, window,
15192 "Caixa::restart_window must return :restart-window \
15193 verbatim by borrow — got {first:?}, expected {window:?}",
15194 );
15195 }
15196 }
15197
15198 #[test]
15199 fn declared_supervisor_slots_restart_window_arm_routes_through_accessor() {
15200 // Composition pin: [`Caixa::declared_supervisor_slots`]'s
15201 // `:restart-window` presence-probe arm must key off
15202 // [`Caixa::restart_window`], not the raw
15203 // `self.restart_window.is_some()` field-probe. Structurally:
15204 // every `Caixa { restart_window: Some(_), .. }` must push
15205 // `SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW` onto the declared-slot
15206 // list, and a `Caixa { restart_window: None, .. }` must NOT
15207 // push the label. Peer of the sibling
15208 // `declared_supervisor_slots_max_restarts_arm_routes_through_accessor`
15209 // routing pin.
15210 for window in ["60s", "5m", "1h", "500ms", "1.5s", ""] {
15211 let c = caixa_with_restart_window(Some(window));
15212 let slots = c.declared_supervisor_slots();
15213 assert!(
15214 slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW),
15215 "declared_supervisor_slots must push \
15216 SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW when \
15217 `:restart-window` is Some({window:?}) — the accessor \
15218 and the enumerator gate must route through the same \
15219 substrate-primitive typed dispatch on the outer \
15220 :restart-window presence bit (got slots={slots:?})",
15221 );
15222 }
15223 let c = caixa_with_restart_window(None);
15224 let slots = c.declared_supervisor_slots();
15225 assert!(
15226 !slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW),
15227 "declared_supervisor_slots must NOT push \
15228 SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW when `:restart-window` \
15229 is None — the author-omitted arm must route through the \
15230 accessor's None-return unchanged (got slots={slots:?})",
15231 );
15232 }
15233
15234 #[test]
15235 fn validate_restart_window_arm_routes_through_accessor() {
15236 // Composition pin: [`Caixa::validate_restart_window`]'s
15237 // shared-codec fold arm must key off [`Caixa::restart_window`],
15238 // not the raw `self.restart_window.as_deref()` field-projection.
15239 // Structurally: (1) `None` → `Ok(())` (the "omit the slot to
15240 // express no reset" canonical shape); (2) a canonical `Some`
15241 // arm (`"60s"`) → `Ok(())`; (3) a codec-rejected `Some` arm
15242 // (`"1.5s"`) → `Err(RestartWindowMalformed { restart_window,
15243 // .. })` carrying the offending raw string verbatim. The three
15244 // arms jointly pin that the validator's raw-string binding is
15245 // the accessor's return, not a peer projection — any future
15246 // silent detour that had the accessor collapse `Some("")` to
15247 // `None` would silently absorb the empty-after-trim refusal
15248 // case at the accessor boundary.
15249 caixa_with_restart_window(None)
15250 .validate_restart_window()
15251 .expect("None :restart-window must validate through the accessor");
15252 caixa_with_restart_window(Some("60s"))
15253 .validate_restart_window()
15254 .expect("canonical :restart-window \"60s\" must validate through the accessor");
15255 let err = caixa_with_restart_window(Some("1.5s"))
15256 .validate_restart_window()
15257 .expect_err("fractional-seconds :restart-window must fail through the accessor");
15258 assert!(
15259 matches!(
15260 err,
15261 ManifestError::RestartWindowMalformed { ref restart_window, .. }
15262 if restart_window == "1.5s"
15263 ),
15264 "validator must carry the offending raw string verbatim \
15265 from the accessor's borrowed &str (got {err:?})",
15266 );
15267 }
15268
15269 #[test]
15270 fn supervisor_view_restart_window_arm_routes_through_accessor() {
15271 // Composition pin: [`Caixa::supervisor_view`]'s
15272 // per-`:restart-window` [`SupervisorSpec`] construction arm
15273 // must key off [`Caixa::restart_window`]'s soft-swallowing
15274 // `.and_then(|s| duration_codec::parse(s).ok())` fold, not the
15275 // raw `self.restart_window.as_deref().and_then(…)` field-fold.
15276 // Structurally: (1) `None` → `SupervisorSpec.restart_window ==
15277 // None` (the "never reset" sentinel); (2) canonical `Some("60s")`
15278 // → `SupervisorSpec.restart_window == Some(Duration::from_secs(60))`
15279 // (the shared codec's canonical parse); (3) codec-rejected
15280 // `Some("1.5s")` → `SupervisorSpec.restart_window == None`
15281 // (the soft-swallow preserving the view's best-effort shape).
15282 let c = caixa_supervisor_with_max_restarts_and_window(None, None);
15283 let view = c.supervisor_view().expect("Supervisor kind has a view");
15284 assert_eq!(
15285 view.restart_window(),
15286 None,
15287 "supervisor_view must project outer None :restart-window \
15288 onto None on the composed SupervisorSpec (never-reset \
15289 sentinel) through the accessor's None-return unchanged",
15290 );
15291
15292 let c = caixa_supervisor_with_max_restarts_and_window(None, Some("60s"));
15293 let view = c.supervisor_view().expect("Supervisor kind has a view");
15294 assert_eq!(
15295 view.restart_window(),
15296 Some(std::time::Duration::from_secs(60)),
15297 "supervisor_view must fold outer Some(\"60s\") through the \
15298 shared duration_codec into Duration::from_secs(60) on the \
15299 composed SupervisorSpec (accessor's Some(&str) → codec \
15300 parse → Some(Duration))",
15301 );
15302
15303 let c = caixa_supervisor_with_max_restarts_and_window(None, Some("1.5s"));
15304 let view = c.supervisor_view().expect("Supervisor kind has a view");
15305 assert_eq!(
15306 view.restart_window(),
15307 None,
15308 "supervisor_view must soft-swallow the shared-codec parse \
15309 failure to None (the view's best-effort shape the sibling \
15310 manifest-level validate_restart_window surfaces as \
15311 RestartWindowMalformed); the accessor's raw-string return \
15312 is the single input every downstream consumer keys off",
15313 );
15314 }
15315
15316 // ── Caixa::upgrade_from — outer top-level &[UpgradeFromEntry] composite-slice accessor ──
15317
15318 fn caixa_with_upgrade_from(upgrade_from: Vec<crate::upgrade::UpgradeFromEntry>) -> Caixa {
15319 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
15320 c.upgrade_from = upgrade_from;
15321 c
15322 }
15323
15324 #[test]
15325 fn upgrade_from_returns_upgrade_from_slice_verbatim_across_permutations() {
15326 // The canonical per-`Caixa` `:upgrade-from` M2 typed-slot
15327 // outer-composite `&[UpgradeFromEntry]`-return slice-shape
15328 // pin: [`Caixa::upgrade_from`] must return the `:upgrade-from`
15329 // typed `Vec<UpgradeFromEntry>` verbatim as a
15330 // `&[UpgradeFromEntry]` slice-view over the same backing
15331 // buffer the raw `self.upgrade_from.as_slice()` field access
15332 // borrows from, element-equal across every representative
15333 // fixture in the accept-set — `[]` (the "no hot-upgrade path
15334 // declared" arm every `defcaixa` without an `:upgrade-from`
15335 // block carries; `#[serde(default)]` folds an omitted slot
15336 // onto `Vec::new()`), a canonical single-entry `Restart`
15337 // fixture (the shape most Servicos carry — a single prior
15338 // version with the fallback strategy), a canonical multi-
15339 // entry list carrying every typed instruction variant
15340 // (`LoadModule` / `StateChange` / `SoftPurge` / `Purge` /
15341 // `Restart`), and a past-the-guard sentinel — a duplicate-
15342 // `:from` `[(0.1.0, Restart), (0.1.0, Restart)]` entry pair
15343 // ([`crate::upgrade::validate_upgrade_from`] rejects through
15344 // `DuplicateFrom { from: "0.1.0" }` but the accessor must
15345 // ship the raw slot verbatim so struct-literal fixtures
15346 // continue to expose the duplicate at the accessor boundary).
15347 //
15348 // Pins against a future silent detour that returned an owned
15349 // `Vec<UpgradeFromEntry>` (which would type-check but silently
15350 // clone on every accessor call, breaking the zero-cost
15351 // projection every peer sibling slice accessor carries), a
15352 // `[dup, dup] → [dup]` dedup collapse (which would silently
15353 // absorb the `DuplicateFrom` refusal case at the accessor
15354 // boundary and the [`crate::StandardLayout::verify`] cross-
15355 // entry gate would silently accept a struct-literal `Caixa`
15356 // carrying the drift), a reference to an operator-resolved
15357 // overlay (the future per-cluster `:upgrade-overrides` slot
15358 // — its resolution must land at exactly this accessor body,
15359 // not silently divert the raw slot away from a second
15360 // consumer), or an axis-shuffled projection (a future detour
15361 // that reordered entries through the accessor would silently
15362 // split the paired [`crate::StandardLayout::verify`] per-
15363 // `:upgrade-from` shape gate's traversal input from the peer
15364 // [`crate::render::servico_m2_overlay`] emitter's projection
15365 // input, since the operator's hot-upgrade dispatch matches
15366 // per-`:from` and axis reordering would silently split the
15367 // per-entry script-path existence probe's iteration order
15368 // from the M2 overlay emitter's serialized-entry order).
15369 //
15370 // First outer top-level [`Caixa`] `&[Composite]`-return
15371 // slice accessor pin on the substrate primitive for M2 / M3
15372 // typed-slot vec-carry axes — opens the outer-`Caixa`
15373 // `&[Composite]` composite-slice projection pattern the
15374 // sibling `:children` [`crate::supervisor::ChildSpec`] /
15375 // `:membros` [`crate::aplicacao::Membro`] / `:contratos`
15376 // [`crate::aplicacao::WitContract`] future outer-composite-
15377 // slice pins fold on. Peer of the closed outer-`Caixa`
15378 // scalar `Option<&Composite>` composite-reference family the
15379 // sibling `limits` / `behavior` / `politicas` / `placement`
15380 // / `entrada` `..._returns_..._option_ref_verbatim_across_
15381 // permutations` pins closed (b2bd9d7 → e4128e4) — extends
15382 // the "byte-equal, borrow-shared" outer-accessor discipline
15383 // onto the outer-`Caixa` `&[Composite]` vec-carry altitude.
15384 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
15385 let fixtures: Vec<Vec<UpgradeFromEntry>> = vec![
15386 vec![],
15387 vec![UpgradeFromEntry {
15388 from: "0.0.1".into(),
15389 instructions: vec![UpgradeInstruction::Restart],
15390 }],
15391 vec![
15392 UpgradeFromEntry {
15393 from: "0.0.1".into(),
15394 instructions: vec![
15395 UpgradeInstruction::LoadModule {
15396 module: "demo".into(),
15397 },
15398 UpgradeInstruction::SoftPurge {
15399 module: "demo".into(),
15400 },
15401 ],
15402 },
15403 UpgradeFromEntry {
15404 from: "0.0.2".into(),
15405 instructions: vec![
15406 UpgradeInstruction::StateChange {
15407 script: "servicos/upgrade.lisp".into(),
15408 },
15409 UpgradeInstruction::Purge {
15410 module: "demo".into(),
15411 },
15412 UpgradeInstruction::Restart,
15413 ],
15414 },
15415 ],
15416 vec![
15417 UpgradeFromEntry {
15418 from: "0.1.0".into(),
15419 instructions: vec![UpgradeInstruction::Restart],
15420 },
15421 UpgradeFromEntry {
15422 from: "0.1.0".into(),
15423 instructions: vec![UpgradeInstruction::Restart],
15424 },
15425 ],
15426 ];
15427 for upgrade_from in fixtures {
15428 let c = caixa_with_upgrade_from(upgrade_from.clone());
15429 assert_eq!(
15430 c.upgrade_from(),
15431 upgrade_from.as_slice(),
15432 "Caixa::upgrade_from must return :upgrade-from \
15433 verbatim (got {:?}, expected {upgrade_from:?})",
15434 c.upgrade_from(),
15435 );
15436 assert_eq!(
15437 c.upgrade_from(),
15438 c.upgrade_from.as_slice(),
15439 "Caixa::upgrade_from must element-equal the raw \
15440 `self.upgrade_from.as_slice()` field access across \
15441 every value in the Vec<UpgradeFromEntry> accept-set",
15442 );
15443 assert_eq!(
15444 c.upgrade_from().is_empty(),
15445 c.upgrade_from.is_empty(),
15446 "Caixa::upgrade_from().is_empty() must byte-equal \
15447 self.upgrade_from.is_empty() — a presence-bit drift \
15448 would silently split the paired \
15449 Caixa::declared_servico_slots M2 declared-slot \
15450 enumerator's presence probe from the peer \
15451 crate::render::servico_m2_overlay M2 overlay \
15452 emitter's presence gate",
15453 );
15454 }
15455 }
15456
15457 #[test]
15458 fn declared_servico_slots_upgrade_from_arm_routes_through_accessor() {
15459 // Composition pin: [`Caixa::declared_servico_slots`]'s
15460 // `:upgrade-from` presence-probe arm must key off
15461 // [`Caixa::upgrade_from`], not the raw
15462 // `self.upgrade_from.is_empty()` field-probe. Structurally: a
15463 // `Caixa { upgrade_from: vec![UpgradeFromEntry { from: "0.0.1",
15464 // instructions: vec![Restart] }], .. }` must push
15465 // `M2_AUTHOR_KEY_UPGRADE_FROM` onto the declared-slot list
15466 // (the presence bit is non-empty, so the M2 kind-coherence
15467 // gate must surface the slot as "declared"), and a `Caixa {
15468 // upgrade_from: vec![], .. }` must NOT push the label (the
15469 // "author omitted the slot entirely" arm — the empty-slice
15470 // partition the serde-default folds onto). The pair jointly
15471 // pins the accessor + declared-slot enumerator composition:
15472 // any future silent detour that had the accessor collapse
15473 // `[Restart]` to `[]` (a `.filter(|e| !e.instructions.
15474 // is_empty())` projection) would silently absorb the
15475 // "declared but degenerate" arm at the accessor boundary and
15476 // the [`crate::LayoutError::ServicoSlotsOnNonServico`] kind-
15477 // coherence gate would silently accept a struct-literal
15478 // `Caixa` carrying the drift.
15479 //
15480 // Peer of the sibling
15481 // `declared_servico_slots_limits_arm_routes_through_accessor`
15482 // (b2bd9d7) and
15483 // `declared_servico_slots_behavior_arm_routes_through_accessor`
15484 // (35d8b52) composition pins on the sibling `:limits` /
15485 // `:behavior` outer-`Option<&Composite>` arms — same "the
15486 // enumerator gate must route through the substrate-primitive
15487 // typed dispatch" discipline extended onto the third M2
15488 // Servico-runtime slot axis, closing the enumerator's routing
15489 // invariant on every M2 arm.
15490 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
15491 let c = caixa_with_upgrade_from(vec![UpgradeFromEntry {
15492 from: "0.0.1".into(),
15493 instructions: vec![UpgradeInstruction::Restart],
15494 }]);
15495 let slots = c.declared_servico_slots();
15496 assert!(
15497 slots.contains(&crate::render::M2_AUTHOR_KEY_UPGRADE_FROM),
15498 "declared_servico_slots must push \
15499 M2_AUTHOR_KEY_UPGRADE_FROM when `:upgrade-from` is \
15500 non-empty — the accessor and the enumerator gate must \
15501 route through the same substrate-primitive typed \
15502 dispatch on the outer :upgrade-from presence bit (got \
15503 slots={slots:?})",
15504 );
15505 let c = caixa_with_upgrade_from(vec![]);
15506 let slots = c.declared_servico_slots();
15507 assert!(
15508 !slots.contains(&crate::render::M2_AUTHOR_KEY_UPGRADE_FROM),
15509 "declared_servico_slots must NOT push \
15510 M2_AUTHOR_KEY_UPGRADE_FROM when `:upgrade-from` is \
15511 empty — the author-omitted arm must route through the \
15512 accessor's empty-slice return unchanged (got \
15513 slots={slots:?})",
15514 );
15515 }
15516
15517 #[test]
15518 fn servico_m2_overlay_upgrade_from_arm_routes_through_accessor() {
15519 // Composition pin: [`crate::render::servico_m2_overlay`]'s
15520 // per-`:upgrade-from` M2 overlay emit arm must key off
15521 // [`Caixa::upgrade_from`], not the raw
15522 // `!caixa.upgrade_from.is_empty()` presence gate + the
15523 // `serde_yaml::to_value(&caixa.upgrade_from)` projection.
15524 // Structurally: a `Caixa { upgrade_from: vec![UpgradeFromEntry
15525 // { from: "0.0.1", instructions: vec![Restart] }], .. }` must
15526 // surface the `M2_KEY_UPGRADE_FROM` key with a per-entry
15527 // sequence in the overlay (the emitter fans onto the serde
15528 // slice-serialization), and a `Caixa { upgrade_from: vec![],
15529 // .. }` must omit the key entirely (the empty-slice
15530 // partition — the `!.is_empty()` outer gate elides the key
15531 // when the author omitted the slot). The pair jointly pins
15532 // the accessor + M2 overlay emitter composition: any future
15533 // silent detour that had the accessor return a fresh-cloned
15534 // `Vec<UpgradeFromEntry>` copy would silently break the
15535 // reference-identity pin the peer per-entry
15536 // `serde_yaml::to_value(caixa.upgrade_from())` projection
15537 // reads from — the projection would clone once per accessor
15538 // call instead of borrowing the storage buffer verbatim.
15539 //
15540 // Peer of the sibling
15541 // `servico_m2_overlay_limits_arm_routes_through_accessor`
15542 // (b2bd9d7) and
15543 // `servico_m2_overlay_behavior_arm_routes_through_accessor`
15544 // (35d8b52) composition pins on the sibling `:limits` /
15545 // `:behavior` outer-`Option<&Composite>` arms — same "the
15546 // M2 overlay emitter must route through the substrate-
15547 // primitive typed dispatch" discipline extended onto the
15548 // third M2 Servico-runtime slot axis, closing the overlay
15549 // emitter's routing invariant on every M2 arm.
15550 use crate::render::{M2_KEY_UPGRADE_FROM, servico_m2_overlay};
15551 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
15552 let c = caixa_with_upgrade_from(vec![UpgradeFromEntry {
15553 from: "0.0.1".into(),
15554 instructions: vec![UpgradeInstruction::Restart],
15555 }]);
15556 let overlay = servico_m2_overlay(&c).unwrap();
15557 assert!(
15558 overlay.contains_key(M2_KEY_UPGRADE_FROM),
15559 "servico_m2_overlay must surface M2_KEY_UPGRADE_FROM when \
15560 `:upgrade-from` is non-empty — the accessor and the M2 \
15561 overlay emitter must route through the same substrate- \
15562 primitive typed dispatch on the outer :upgrade-from \
15563 slice (got overlay={overlay:?})",
15564 );
15565 let c = caixa_with_upgrade_from(vec![]);
15566 let overlay = servico_m2_overlay(&c).unwrap();
15567 assert!(
15568 !overlay.contains_key(M2_KEY_UPGRADE_FROM),
15569 "servico_m2_overlay must omit M2_KEY_UPGRADE_FROM when \
15570 `:upgrade-from` is empty — the empty-slice partition \
15571 must route through the accessor's empty-slice return \
15572 unchanged (got overlay={overlay:?})",
15573 );
15574 }
15575
15576 #[test]
15577 fn upgrade_from_projects_slice_by_borrow() {
15578 // The by-borrow pin: [`Caixa::upgrade_from`] returns
15579 // `&[UpgradeFromEntry]` by borrow — the returned slice
15580 // borrows the underlying `Vec<UpgradeFromEntry>` storage of
15581 // the `:upgrade-from` slot and the accessor must not clone
15582 // the backing `Vec` on every call. Peer of the sibling
15583 // outer top-level [`Caixa`] `&[T]`-return by-borrow pins
15584 // (`autores_projects_slice_by_borrow` b5d813f,
15585 // `etiquetas_projects_slice_by_borrow` 78c7d3c,
15586 // `bibliotecas_projects_slice_by_borrow` 8a36c23,
15587 // `exe_projects_slice_by_borrow` 65d9527,
15588 // `servicos_projects_slice_by_borrow` 611f78b,
15589 // `deps_projects_slice_by_borrow` ad34b4e,
15590 // `deps_dev_projects_slice_by_borrow` f7fd81e) on the
15591 // sibling outer top-level [`Caixa`] scalar-element `&[T]`
15592 // axes — extended here to the first outer-`Caixa`
15593 // composite-element `&[Composite]` axis: the accessor's
15594 // returned slice must borrow from `&self` (the returned
15595 // reference's lifetime is tied to `&self`), and calling the
15596 // accessor twice on the same [`Caixa`] must yield slices
15597 // that are pointer-equal (the underlying byte-buffer is the
15598 // storage `Vec`'s allocation, not a fresh copy) as well as
15599 // value-equal (idempotent, no side effects on `&self`).
15600 //
15601 // Pins against a future silent detour that returned an owned
15602 // `Vec<UpgradeFromEntry>` (which would type-check but
15603 // silently clone on every call), a `&Vec<UpgradeFromEntry>`
15604 // return (which would leak the backing `Vec`'s
15605 // grow/push/reserve surface no downstream consumer reaches
15606 // for), or a one-arm-only accessor that returned a
15607 // saturating value on some sentinel input.
15608 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
15609 for upgrade_from in [
15610 vec![],
15611 vec![UpgradeFromEntry {
15612 from: "0.0.1".into(),
15613 instructions: vec![UpgradeInstruction::Restart],
15614 }],
15615 vec![
15616 UpgradeFromEntry {
15617 from: "0.0.1".into(),
15618 instructions: vec![UpgradeInstruction::Restart],
15619 },
15620 UpgradeFromEntry {
15621 from: "0.0.2".into(),
15622 instructions: vec![UpgradeInstruction::SoftPurge {
15623 module: "demo".into(),
15624 }],
15625 },
15626 ],
15627 ] {
15628 let c = caixa_with_upgrade_from(upgrade_from.clone());
15629 let first = c.upgrade_from();
15630 let second = c.upgrade_from();
15631 assert_eq!(
15632 first, second,
15633 "Caixa::upgrade_from must be idempotent — two \
15634 successive calls on the same &self must return the \
15635 same &[UpgradeFromEntry]",
15636 );
15637 assert_eq!(
15638 first.as_ptr(),
15639 second.as_ptr(),
15640 "Caixa::upgrade_from must borrow the underlying \
15641 Vec<UpgradeFromEntry> storage — two successive calls \
15642 must return slices with the same backing pointer (a \
15643 fresh Vec<UpgradeFromEntry> clone would change the \
15644 pointer on every call)",
15645 );
15646 assert_eq!(
15647 first,
15648 upgrade_from.as_slice(),
15649 "Caixa::upgrade_from must return :upgrade-from \
15650 verbatim by borrow — got {first:?}, expected \
15651 {upgrade_from:?}",
15652 );
15653 }
15654 }
15655
15656 // ── Caixa::children — outer top-level &[ChildSpec] composite-slice accessor ──
15657
15658 fn caixa_with_children(children: Vec<crate::supervisor::ChildSpec>) -> Caixa {
15659 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
15660 c.children = children;
15661 c
15662 }
15663
15664 #[test]
15665 fn children_returns_children_slice_verbatim_across_permutations() {
15666 // The canonical per-`Caixa` `:children` M2 supervisor-tree-slot
15667 // outer-composite `&[ChildSpec]`-return slice-shape pin:
15668 // [`Caixa::children`] must return the `:children` typed
15669 // `Vec<ChildSpec>` verbatim as a `&[ChildSpec]` slice-view over
15670 // the same backing buffer the raw `self.children.as_slice()`
15671 // field access borrows from, element-equal across every
15672 // representative fixture in the accept-set — `[]` (the "no
15673 // static children declared" arm every non-`Supervisor`-kind
15674 // `defcaixa` carries by `#[serde(default)]` and every
15675 // `SimpleOneForOne` supervisor carries by cross-slot refusal),
15676 // a canonical single-child `Permanent` fixture (the shape
15677 // most `OneForOne` supervisors carry — a single long-running
15678 // worker child), a canonical multi-child list carrying every
15679 // typed restart-policy variant (`Permanent` / `Transient` /
15680 // `Temporary`), and a past-the-guard sentinel — a duplicate
15681 // `:caixa` `[("w", ...), ("w", ...)]` entry pair
15682 // ([`crate::SupervisorSpec::validate`] rejects through
15683 // `DuplicateChildNome { nome: "w" }` but the accessor must
15684 // ship the raw slot verbatim so struct-literal fixtures
15685 // continue to expose the duplicate at the accessor boundary).
15686 //
15687 // Pins against a future silent detour that returned an owned
15688 // `Vec<ChildSpec>` (which would type-check but silently clone
15689 // on every accessor call, breaking the zero-cost projection
15690 // every peer sibling slice accessor carries), a `[dup, dup] →
15691 // [dup]` dedup collapse (which would silently absorb the
15692 // `DuplicateChildNome` refusal case at the accessor boundary
15693 // and the [`crate::StandardLayout::verify`] cross-child gate
15694 // would silently accept a struct-literal `Caixa` carrying the
15695 // drift), a reference to an operator-resolved overlay (the
15696 // future per-cluster `:children-overrides` slot — its
15697 // resolution must land at exactly this accessor body, not
15698 // silently divert the raw slot away from a second consumer),
15699 // or an axis-shuffled projection (a future detour that
15700 // reordered children through the accessor would silently
15701 // split the paired [`crate::StandardLayout::verify`] per-
15702 // supervisor gate's traversal input from the peer
15703 // [`Self::supervisor_view`] fold-in path's clone-order input,
15704 // since the OTP `RestForOne` restart strategy dispatches on
15705 // declared child order and axis reordering would silently
15706 // split the operator's per-cluster restart-fan-out order
15707 // from the caixa.lisp source-order).
15708 //
15709 // Second outer top-level [`Caixa`] `&[Composite]`-return slice
15710 // accessor pin on the substrate primitive for M2 / M3 typed-
15711 // slot vec-carry axes — folds on the outer-`Caixa`
15712 // `&[Composite]` composite-slice sub-family the sibling
15713 // `upgrade_from_returns_upgrade_from_slice_verbatim_across_permutations`
15714 // (2a1f907) pin opened, peer at the outer altitude of the
15715 // closed inner-`SupervisorSpec` `SupervisorSpec::children`
15716 // (bc92bce) accessor on the same OTP-supervisor static-child-
15717 // list axis.
15718 use crate::supervisor::{ChildSpec, RestartPolicy};
15719 let fixtures: Vec<Vec<ChildSpec>> = vec![
15720 vec![],
15721 vec![ChildSpec {
15722 caixa: "worker".into(),
15723 versao: "^0.1".into(),
15724 restart: RestartPolicy::Permanent,
15725 }],
15726 vec![
15727 ChildSpec {
15728 caixa: "worker-a".into(),
15729 versao: "^0.1".into(),
15730 restart: RestartPolicy::Permanent,
15731 },
15732 ChildSpec {
15733 caixa: "worker-b".into(),
15734 versao: "^0.1".into(),
15735 restart: RestartPolicy::Transient,
15736 },
15737 ChildSpec {
15738 caixa: "worker-c".into(),
15739 versao: "^0.1".into(),
15740 restart: RestartPolicy::Temporary,
15741 },
15742 ],
15743 vec![
15744 ChildSpec {
15745 caixa: "w".into(),
15746 versao: "^0.1".into(),
15747 restart: RestartPolicy::Permanent,
15748 },
15749 ChildSpec {
15750 caixa: "w".into(),
15751 versao: "^0.1".into(),
15752 restart: RestartPolicy::Permanent,
15753 },
15754 ],
15755 ];
15756 for children in fixtures {
15757 let c = caixa_with_children(children.clone());
15758 assert_eq!(
15759 c.children(),
15760 children.as_slice(),
15761 "Caixa::children must return :children verbatim \
15762 (got {:?}, expected {children:?})",
15763 c.children(),
15764 );
15765 assert_eq!(
15766 c.children(),
15767 c.children.as_slice(),
15768 "Caixa::children must element-equal the raw \
15769 `self.children.as_slice()` field access across \
15770 every value in the Vec<ChildSpec> accept-set",
15771 );
15772 assert_eq!(
15773 c.children().is_empty(),
15774 c.children.is_empty(),
15775 "Caixa::children().is_empty() must byte-equal \
15776 self.children.is_empty() — a presence-bit drift \
15777 would silently split the paired \
15778 Caixa::declared_supervisor_slots supervisor-tree \
15779 declared-slot enumerator's presence probe from the \
15780 peer Caixa::supervisor_view typed-view composer's \
15781 fold-in path",
15782 );
15783 }
15784 }
15785
15786 #[test]
15787 fn declared_supervisor_slots_children_arm_routes_through_accessor() {
15788 // Composition pin: [`Caixa::declared_supervisor_slots`]'s
15789 // `:children` presence-probe arm must key off
15790 // [`Caixa::children`], not the raw
15791 // `!self.children.is_empty()` field-probe. Structurally: a
15792 // `Caixa { children: vec![ChildSpec { caixa: "w", versao:
15793 // "^0.1", restart: Permanent }], .. }` must push
15794 // `SUPERVISOR_AUTHOR_KEY_CHILDREN` onto the declared-slot list
15795 // (the presence bit is non-empty, so the supervisor-tree
15796 // kind-coherence gate must surface the slot as "declared"),
15797 // and a `Caixa { children: vec![], .. }` must NOT push the
15798 // label (the "author omitted the slot entirely" arm — the
15799 // empty-slice partition the serde-default folds onto). The
15800 // pair jointly pins the accessor + declared-slot enumerator
15801 // composition: any future silent detour that had the accessor
15802 // collapse `[Permanent]` to `[]` (a `.filter(|c| c.nome() !=
15803 // "__reserved__")` projection) would silently absorb the
15804 // "declared but degenerate" arm at the accessor boundary and
15805 // the [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
15806 // kind-coherence gate would silently accept a struct-literal
15807 // `Caixa` carrying the drift.
15808 //
15809 // Peer of the sibling
15810 // `declared_servico_slots_upgrade_from_arm_routes_through_accessor`
15811 // (2a1f907) on the M2 `:upgrade-from` composite-slice arm —
15812 // same "the enumerator gate must route through the substrate-
15813 // primitive typed dispatch" discipline extended onto the
15814 // supervisor-tree `:children` composite-slice arm.
15815 use crate::supervisor::{ChildSpec, RestartPolicy};
15816 let c = caixa_with_children(vec![ChildSpec {
15817 caixa: "w".into(),
15818 versao: "^0.1".into(),
15819 restart: RestartPolicy::Permanent,
15820 }]);
15821 let slots = c.declared_supervisor_slots();
15822 assert!(
15823 slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN),
15824 "declared_supervisor_slots must push \
15825 SUPERVISOR_AUTHOR_KEY_CHILDREN when `:children` is \
15826 non-empty — the accessor and the enumerator gate must \
15827 route through the same substrate-primitive typed \
15828 dispatch on the outer :children presence bit (got \
15829 slots={slots:?})",
15830 );
15831 let c = caixa_with_children(vec![]);
15832 let slots = c.declared_supervisor_slots();
15833 assert!(
15834 !slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN),
15835 "declared_supervisor_slots must NOT push \
15836 SUPERVISOR_AUTHOR_KEY_CHILDREN when `:children` is \
15837 empty — the author-omitted arm must route through the \
15838 accessor's empty-slice return unchanged (got \
15839 slots={slots:?})",
15840 );
15841 }
15842
15843 #[test]
15844 fn supervisor_view_children_arm_routes_through_accessor() {
15845 // Composition pin: [`Caixa::supervisor_view`]'s per-`:children`
15846 // fold-in arm must key off [`Caixa::children`], not the raw
15847 // `self.children.clone()` field-clone. Structurally: a `Caixa {
15848 // kind: Supervisor, estrategia: Some(OneForOne), children:
15849 // vec![ChildSpec { caixa: "w", .. }], .. }` must fold the
15850 // per-child list through the accessor into the typed
15851 // [`SupervisorSpec`] view's `children` field verbatim — every
15852 // entry the accessor surfaces must land in the view's
15853 // `children` slot in the same order. The pair jointly pins the
15854 // accessor + view-composer composition: any future silent
15855 // detour that had the accessor return a fresh-cloned
15856 // `Vec<ChildSpec>` copy would silently break the reference-
15857 // identity pin the peer `supervisor_view` fold-in path reads
15858 // from — the fold would clone once more per accessor call
15859 // instead of borrowing the storage buffer verbatim once.
15860 //
15861 // Peer of the sibling
15862 // `supervisor_view_kind_gate_routes_through_accessor` (35d8b52-
15863 // family) composition pin on the peer kind-gate arm — same
15864 // "the view composer must route through the substrate-
15865 // primitive typed dispatch" discipline extended onto the
15866 // per-`:children` fold-in arm, closing the supervisor-view
15867 // composer's routing invariant on the composite-slice input.
15868 use crate::supervisor::{ChildSpec, RestartPolicy, RestartStrategy};
15869 let mut c = caixa_with_children(vec![
15870 ChildSpec {
15871 caixa: "worker-a".into(),
15872 versao: "^0.1".into(),
15873 restart: RestartPolicy::Permanent,
15874 },
15875 ChildSpec {
15876 caixa: "worker-b".into(),
15877 versao: "^0.1".into(),
15878 restart: RestartPolicy::Transient,
15879 },
15880 ]);
15881 c.kind = crate::CaixaKind::Supervisor;
15882 c.estrategia = Some(RestartStrategy::OneForOne);
15883 let view = c
15884 .supervisor_view()
15885 .expect("Supervisor kind must produce a supervisor_view");
15886 assert_eq!(
15887 view.children(),
15888 c.children(),
15889 "supervisor_view must fold Caixa::children verbatim into \
15890 SupervisorSpec::children — the accessor and the view \
15891 composer must route through the same substrate-primitive \
15892 typed dispatch on the outer :children slice (got view \
15893 children={:?}, expected {:?})",
15894 view.children(),
15895 c.children(),
15896 );
15897 }
15898
15899 #[test]
15900 fn children_projects_slice_by_borrow() {
15901 // The by-borrow pin: [`Caixa::children`] returns
15902 // `&[ChildSpec]` by borrow — the returned slice borrows the
15903 // underlying `Vec<ChildSpec>` storage of the `:children` slot
15904 // and the accessor must not clone the backing `Vec` on every
15905 // call. Peer of the sibling outer top-level [`Caixa`]
15906 // `&[T]`-return by-borrow pins (`autores_projects_slice_by_borrow`
15907 // b5d813f, `etiquetas_projects_slice_by_borrow` 78c7d3c,
15908 // `bibliotecas_projects_slice_by_borrow` 8a36c23,
15909 // `exe_projects_slice_by_borrow` 65d9527,
15910 // `servicos_projects_slice_by_borrow` 611f78b,
15911 // `deps_projects_slice_by_borrow` ad34b4e,
15912 // `deps_dev_projects_slice_by_borrow` f7fd81e,
15913 // `upgrade_from_projects_slice_by_borrow` 2a1f907) on the
15914 // sibling outer top-level [`Caixa`] scalar-element and
15915 // composite-element `&[T]` axes — folds on the outer-`Caixa`
15916 // composite-element `&[Composite]` axis: the accessor's
15917 // returned slice must borrow from `&self` (the returned
15918 // reference's lifetime is tied to `&self`), and calling the
15919 // accessor twice on the same [`Caixa`] must yield slices
15920 // that are pointer-equal (the underlying byte-buffer is the
15921 // storage `Vec`'s allocation, not a fresh copy) as well as
15922 // value-equal (idempotent, no side effects on `&self`).
15923 //
15924 // Pins against a future silent detour that returned an owned
15925 // `Vec<ChildSpec>` (which would type-check but silently clone
15926 // on every call), a `&Vec<ChildSpec>` return (which would leak
15927 // the backing `Vec`'s grow/push/reserve surface no downstream
15928 // consumer reaches for), or a one-arm-only accessor that
15929 // returned a saturating value on some sentinel input.
15930 use crate::supervisor::{ChildSpec, RestartPolicy};
15931 for children in [
15932 vec![],
15933 vec![ChildSpec {
15934 caixa: "w".into(),
15935 versao: "^0.1".into(),
15936 restart: RestartPolicy::Permanent,
15937 }],
15938 vec![
15939 ChildSpec {
15940 caixa: "worker-a".into(),
15941 versao: "^0.1".into(),
15942 restart: RestartPolicy::Permanent,
15943 },
15944 ChildSpec {
15945 caixa: "worker-b".into(),
15946 versao: "^0.1".into(),
15947 restart: RestartPolicy::Transient,
15948 },
15949 ],
15950 ] {
15951 let c = caixa_with_children(children.clone());
15952 let first = c.children();
15953 let second = c.children();
15954 assert_eq!(
15955 first, second,
15956 "Caixa::children must be idempotent — two successive \
15957 calls on the same &self must return the same \
15958 &[ChildSpec]",
15959 );
15960 assert_eq!(
15961 first.as_ptr(),
15962 second.as_ptr(),
15963 "Caixa::children must borrow the underlying \
15964 Vec<ChildSpec> storage — two successive calls must \
15965 return slices with the same backing pointer (a fresh \
15966 Vec<ChildSpec> clone would change the pointer on \
15967 every call)",
15968 );
15969 assert_eq!(
15970 first,
15971 children.as_slice(),
15972 "Caixa::children must return :children verbatim by \
15973 borrow — got {first:?}, expected {children:?}",
15974 );
15975 }
15976 }
15977
15978 // ── Caixa::membros — outer top-level &[Membro] composite-slice accessor ──
15979
15980 fn caixa_aplicacao_with_membros(membros: Vec<crate::aplicacao::Membro>) -> Caixa {
15981 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
15982 c.kind = CaixaKind::Aplicacao;
15983 c.membros = membros;
15984 c
15985 }
15986
15987 #[test]
15988 fn membros_returns_membros_slice_verbatim_across_permutations() {
15989 // The canonical per-`Caixa` `:membros` M3 mesh-slot outer-
15990 // composite `&[Membro]`-return slice-shape pin:
15991 // [`Caixa::membros`] must return the `:membros` typed
15992 // `Vec<Membro>` verbatim as a `&[Membro]` slice-view over the
15993 // same backing buffer the raw `self.membros.as_slice()` field
15994 // access borrows from, element-equal across every
15995 // representative fixture in the accept-set — `[]` (the "no
15996 // members declared" arm every non-`Aplicacao`-kind `defcaixa`
15997 // carries by `#[serde(default)]` and every partially-authored
15998 // Aplicacao carries before the
15999 // [`crate::AplicacaoError::MembrosEmpty`] gate fires), a
16000 // canonical single-member fixture (the shape a minimal
16001 // Aplicacao carries — one Servico wrapping one contained
16002 // computation), a canonical multi-member list carrying three
16003 // distinct entries (the canonical checkout-shape Aplicacao —
16004 // cart / pricing / auth — every canonical example carries), and
16005 // a past-the-guard sentinel — a duplicate `:caixa`
16006 // `[("cart", ...), ("cart", ...)]` entry pair
16007 // ([`crate::AplicacaoSpec::validate`] rejects through
16008 // `DuplicateMembro { nome: "cart" }` but the accessor must ship
16009 // the raw slot verbatim so struct-literal fixtures continue to
16010 // expose the duplicate at the accessor boundary).
16011 //
16012 // Pins against a future silent detour that returned an owned
16013 // `Vec<Membro>` (which would type-check but silently clone on
16014 // every accessor call, breaking the zero-cost projection every
16015 // peer sibling slice accessor carries), a `[dup, dup] → [dup]`
16016 // dedup collapse (which would silently absorb the
16017 // `DuplicateMembro` refusal case at the accessor boundary and
16018 // the [`crate::StandardLayout::verify`] cross-member gate would
16019 // silently accept a struct-literal `Caixa` carrying the drift),
16020 // a reference to an operator-resolved overlay (the future per-
16021 // cluster `:membros-overrides` slot — its resolution must land
16022 // at exactly this accessor body, not silently divert the raw
16023 // slot away from a second consumer), or an axis-shuffled
16024 // projection (a future detour that reordered members through
16025 // the accessor would silently split the paired
16026 // [`crate::StandardLayout::verify`] per-Aplicacao gate's
16027 // traversal input from the peer [`Self::aplicacao_view`] fold-
16028 // in path's clone-order input, since the canonical `:contratos`
16029 // `:de`/`:para` and `:entrada :para` cross-slot refusal probes
16030 // read the member set through the same slice).
16031 //
16032 // Third outer top-level [`Caixa`] `&[Composite]`-return slice
16033 // accessor pin on the substrate primitive for M2 / M3 typed-
16034 // slot vec-carry axes — opens the outer-`Caixa` M3 mesh-slot
16035 // arm of the `&[Composite]` composite-slice sub-family the
16036 // sibling M2 `upgrade_from_returns_upgrade_from_slice_verbatim_across_permutations`
16037 // (2a1f907) and
16038 // `children_returns_children_slice_verbatim_across_permutations`
16039 // (c17b51e) pins opened, peer at the outer altitude of the
16040 // closed inner-[`crate::AplicacaoSpec::membros`] (6c77e36)
16041 // accessor on the same MESH-COMPOSITION per-Aplicacao member-
16042 // list axis.
16043 use crate::aplicacao::Membro;
16044 let fixtures: Vec<Vec<Membro>> = vec![
16045 vec![],
16046 vec![Membro {
16047 caixa: "cart".into(),
16048 versao: "^0.1".into(),
16049 }],
16050 vec![
16051 Membro {
16052 caixa: "cart".into(),
16053 versao: "^0.1".into(),
16054 },
16055 Membro {
16056 caixa: "pricing".into(),
16057 versao: "^0.2".into(),
16058 },
16059 Membro {
16060 caixa: "auth".into(),
16061 versao: "^1.0".into(),
16062 },
16063 ],
16064 vec![
16065 Membro {
16066 caixa: "cart".into(),
16067 versao: "^0.1".into(),
16068 },
16069 Membro {
16070 caixa: "cart".into(),
16071 versao: "^0.1".into(),
16072 },
16073 ],
16074 ];
16075 for membros in fixtures {
16076 let c = caixa_aplicacao_with_membros(membros.clone());
16077 assert_eq!(
16078 c.membros(),
16079 membros.as_slice(),
16080 "Caixa::membros must return :membros verbatim \
16081 (got {:?}, expected {membros:?})",
16082 c.membros(),
16083 );
16084 assert_eq!(
16085 c.membros(),
16086 c.membros.as_slice(),
16087 "Caixa::membros must element-equal the raw \
16088 `self.membros.as_slice()` field access across every \
16089 value in the Vec<Membro> accept-set",
16090 );
16091 assert_eq!(
16092 c.membros().is_empty(),
16093 c.membros.is_empty(),
16094 "Caixa::membros().is_empty() must byte-equal \
16095 self.membros.is_empty() — a presence-bit drift would \
16096 silently split the paired Caixa::declared_mesh_slots \
16097 mesh declared-slot enumerator's presence probe from \
16098 the peer Caixa::aplicacao_view typed-view composer's \
16099 fold-in path",
16100 );
16101 }
16102 }
16103
16104 #[test]
16105 fn declared_mesh_slots_membros_arm_routes_through_accessor() {
16106 // Composition pin: [`Caixa::declared_mesh_slots`]'s `:membros`
16107 // presence-probe arm must key off [`Caixa::membros`], not the
16108 // raw `!self.membros.is_empty()` field-probe. Structurally: a
16109 // `Caixa { membros: vec![Membro { caixa: "cart", versao:
16110 // "^0.1" }], .. }` must push `M3_AUTHOR_KEY_MEMBROS` onto the
16111 // declared-slot list (the presence bit is non-empty, so the
16112 // mesh kind-coherence gate must surface the slot as
16113 // "declared"), and a `Caixa { membros: vec![], .. }` must NOT
16114 // push the label (the "author omitted the slot entirely" arm
16115 // — the empty-slice partition the serde-default folds onto).
16116 // The pair jointly pins the accessor + declared-slot
16117 // enumerator composition: any future silent detour that had
16118 // the accessor collapse `[Membro { .. }]` to `[]` (a
16119 // `.filter(|m| m.nome() != "__reserved__")` projection) would
16120 // silently absorb the "declared but degenerate" arm at the
16121 // accessor boundary and the
16122 // [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
16123 // coherence gate would silently accept a struct-literal
16124 // `Caixa` carrying the drift.
16125 //
16126 // Peer of the sibling
16127 // `declared_servico_slots_upgrade_from_arm_routes_through_accessor`
16128 // (2a1f907) and
16129 // `declared_supervisor_slots_children_arm_routes_through_accessor`
16130 // (c17b51e) composition pins on the M2 `:upgrade-from` /
16131 // `:children` composite-slice arms — same "the enumerator gate
16132 // must route through the substrate-primitive typed dispatch"
16133 // discipline extended onto the M3 `:membros` composite-slice
16134 // arm, opening the M3 arm of the declared-slot enumerator's
16135 // routing invariant.
16136 use crate::aplicacao::Membro;
16137 let c = caixa_aplicacao_with_membros(vec![Membro {
16138 caixa: "cart".into(),
16139 versao: "^0.1".into(),
16140 }]);
16141 let slots = c.declared_mesh_slots();
16142 assert!(
16143 slots.contains(&crate::render::M3_AUTHOR_KEY_MEMBROS),
16144 "declared_mesh_slots must push M3_AUTHOR_KEY_MEMBROS when \
16145 `:membros` is non-empty — the accessor and the enumerator \
16146 gate must route through the same substrate-primitive \
16147 typed dispatch on the outer :membros presence bit (got \
16148 slots={slots:?})",
16149 );
16150 let c = caixa_aplicacao_with_membros(vec![]);
16151 let slots = c.declared_mesh_slots();
16152 assert!(
16153 !slots.contains(&crate::render::M3_AUTHOR_KEY_MEMBROS),
16154 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_MEMBROS \
16155 when `:membros` is empty — the author-omitted arm must \
16156 route through the accessor's empty-slice return unchanged \
16157 (got slots={slots:?})",
16158 );
16159 }
16160
16161 #[test]
16162 fn aplicacao_view_membros_arm_routes_through_accessor() {
16163 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:membros`
16164 // fold-in arm must key off [`Caixa::membros`], not the raw
16165 // `self.membros.clone()` field-clone. Structurally: a `Caixa {
16166 // kind: Aplicacao, membros: vec![Membro { caixa: "cart", .. },
16167 // Membro { caixa: "pricing", .. }], .. }` must fold the per-
16168 // member list through the accessor into the typed
16169 // [`crate::AplicacaoSpec`] view's `membros` slot verbatim —
16170 // every entry the accessor surfaces must land in the view's
16171 // `membros` slot in the same order. The pair jointly pins the
16172 // accessor + view-composer composition: any future silent
16173 // detour that had the accessor return a fresh-cloned
16174 // `Vec<Membro>` copy would silently break the reference-
16175 // identity pin the peer `aplicacao_view` fold-in path reads
16176 // from — the fold would clone once more per accessor call
16177 // instead of borrowing the storage buffer verbatim once.
16178 //
16179 // Peer of the sibling
16180 // `aplicacao_view_politicas_arm_folds_through_accessor`
16181 // (5d23d29) /
16182 // `aplicacao_view_placement_arm_folds_through_accessor`
16183 // (4fb8074) /
16184 // `aplicacao_view_entrada_arm_folds_through_accessor` (e4128e4)
16185 // composition pins on the M3 `:politicas` / `:placement` /
16186 // `:entrada` outer-`Option<&Composite>` arms — extended here to
16187 // the M3 `:membros` outer-`&[Composite]` composite-slice arm,
16188 // closing the aplicacao-view composer's routing invariant on
16189 // the composite-slice input.
16190 use crate::aplicacao::Membro;
16191 let c = caixa_aplicacao_with_membros(vec![
16192 Membro {
16193 caixa: "cart".into(),
16194 versao: "^0.1".into(),
16195 },
16196 Membro {
16197 caixa: "pricing".into(),
16198 versao: "^0.2".into(),
16199 },
16200 ]);
16201 let view = c
16202 .aplicacao_view()
16203 .expect("Aplicacao kind must produce an aplicacao_view");
16204 assert_eq!(
16205 view.membros(),
16206 c.membros(),
16207 "aplicacao_view must fold Caixa::membros verbatim into \
16208 AplicacaoSpec::membros — the accessor and the view \
16209 composer must route through the same substrate-primitive \
16210 typed dispatch on the outer :membros slice (got view \
16211 membros={:?}, expected {:?})",
16212 view.membros(),
16213 c.membros(),
16214 );
16215 }
16216
16217 #[test]
16218 fn membros_projects_slice_by_borrow() {
16219 // The by-borrow pin: [`Caixa::membros`] returns `&[Membro]` by
16220 // borrow — the returned slice borrows the underlying
16221 // `Vec<Membro>` storage of the `:membros` slot and the
16222 // accessor must not clone the backing `Vec` on every call.
16223 // Peer of the sibling outer top-level [`Caixa`] `&[T]`-return
16224 // by-borrow pins (`autores_projects_slice_by_borrow` b5d813f,
16225 // `etiquetas_projects_slice_by_borrow` 78c7d3c,
16226 // `bibliotecas_projects_slice_by_borrow` 8a36c23,
16227 // `exe_projects_slice_by_borrow` 65d9527,
16228 // `servicos_projects_slice_by_borrow` 611f78b,
16229 // `deps_projects_slice_by_borrow` ad34b4e,
16230 // `deps_dev_projects_slice_by_borrow` f7fd81e,
16231 // `upgrade_from_projects_slice_by_borrow` 2a1f907,
16232 // `children_projects_slice_by_borrow` c17b51e) on the sibling
16233 // outer top-level [`Caixa`] scalar-element and composite-
16234 // element `&[T]` axes — folds on the outer-`Caixa` M3 mesh-
16235 // slot composite-element `&[Composite]` axis: the accessor's
16236 // returned slice must borrow from `&self` (the returned
16237 // reference's lifetime is tied to `&self`), and calling the
16238 // accessor twice on the same [`Caixa`] must yield slices that
16239 // are pointer-equal (the underlying byte-buffer is the storage
16240 // `Vec`'s allocation, not a fresh copy) as well as value-equal
16241 // (idempotent, no side effects on `&self`).
16242 //
16243 // Pins against a future silent detour that returned an owned
16244 // `Vec<Membro>` (which would type-check but silently clone on
16245 // every call), a `&Vec<Membro>` return (which would leak the
16246 // backing `Vec`'s grow/push/reserve surface no downstream
16247 // consumer reaches for), or a one-arm-only accessor that
16248 // returned a saturating value on some sentinel input.
16249 use crate::aplicacao::Membro;
16250 for membros in [
16251 vec![],
16252 vec![Membro {
16253 caixa: "cart".into(),
16254 versao: "^0.1".into(),
16255 }],
16256 vec![
16257 Membro {
16258 caixa: "cart".into(),
16259 versao: "^0.1".into(),
16260 },
16261 Membro {
16262 caixa: "pricing".into(),
16263 versao: "^0.2".into(),
16264 },
16265 ],
16266 ] {
16267 let c = caixa_aplicacao_with_membros(membros.clone());
16268 let first = c.membros();
16269 let second = c.membros();
16270 assert_eq!(
16271 first, second,
16272 "Caixa::membros must be idempotent — two successive \
16273 calls on the same &self must return the same &[Membro]",
16274 );
16275 assert_eq!(
16276 first.as_ptr(),
16277 second.as_ptr(),
16278 "Caixa::membros must borrow the underlying Vec<Membro> \
16279 storage — two successive calls must return slices with \
16280 the same backing pointer (a fresh Vec<Membro> clone \
16281 would change the pointer on every call)",
16282 );
16283 assert_eq!(
16284 first,
16285 membros.as_slice(),
16286 "Caixa::membros must return :membros verbatim by borrow \
16287 — got {first:?}, expected {membros:?}",
16288 );
16289 }
16290 }
16291
16292 // ── Caixa::contratos — outer top-level &[WitContract] composite-slice accessor ──
16293
16294 fn caixa_aplicacao_with_contratos(contratos: Vec<crate::aplicacao::WitContract>) -> Caixa {
16295 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
16296 c.kind = CaixaKind::Aplicacao;
16297 c.contratos = contratos;
16298 c
16299 }
16300
16301 fn contrato_http_for_test(
16302 de: &str,
16303 para: &str,
16304 endpoint: &str,
16305 ) -> crate::aplicacao::WitContract {
16306 crate::aplicacao::WitContract {
16307 de: de.into(),
16308 para: para.into(),
16309 wit: "wasi:http/proxy".into(),
16310 endpoint: Some(endpoint.into()),
16311 subject: None,
16312 slot: None,
16313 }
16314 }
16315
16316 #[test]
16317 fn contratos_returns_contratos_slice_verbatim_across_permutations() {
16318 // The canonical per-`Caixa` `:contratos` M3 mesh-slot outer-
16319 // composite `&[WitContract]`-return slice-shape pin:
16320 // [`Caixa::contratos`] must return the `:contratos` typed
16321 // `Vec<WitContract>` verbatim as a `&[WitContract]` slice-view
16322 // over the same backing buffer the raw
16323 // `self.contratos.as_slice()` field access borrows from,
16324 // element-equal across every representative fixture in the
16325 // accept-set — `[]` (the "no contracts declared" arm every
16326 // non-`Aplicacao`-kind `defcaixa` carries by
16327 // `#[serde(default)]` and every leaf-Aplicacao with a single
16328 // member carries), a canonical single-edge fixture (the
16329 // minimal directed-graph shape: one HTTP-shape `(cart → catalog)`
16330 // edge), and a canonical multi-edge fixture with three distinct
16331 // edges (the checkout-shape Aplicacao's HTTP-fan pattern:
16332 // `(cart → catalog)`, `(cart → pricing)`, `(cart → auth)`).
16333 //
16334 // Pins against a future silent detour that returned an owned
16335 // `Vec<WitContract>` (which would type-check but silently clone
16336 // on every accessor call, breaking the zero-cost projection
16337 // every peer sibling slice accessor carries), an axis-shuffled
16338 // projection (a future detour that reordered edges through the
16339 // accessor would silently split the paired
16340 // [`crate::StandardLayout::verify`] per-Aplicacao gate's
16341 // traversal input from the peer [`Self::aplicacao_view`] fold-
16342 // in path's clone-order input, since every canonical
16343 // `caixa-mesh` renderer's per-`(:de, :para)` adjacency-list
16344 // seed dispatch reads the edge set through the same slice),
16345 // or a reference to an operator-resolved overlay (the future
16346 // per-cluster `:contratos-overrides` slot — its resolution
16347 // must land at exactly this accessor body, not silently divert
16348 // the raw slot away from a second consumer).
16349 //
16350 // Fourth outer top-level [`Caixa`] `&[Composite]`-return slice
16351 // accessor pin on the substrate primitive for M2 / M3 typed-
16352 // slot vec-carry axes — closes the outer-`Caixa`
16353 // `&[Composite]` composite-slice sub-family the sibling M2
16354 // `upgrade_from_returns_upgrade_from_slice_verbatim_across_permutations`
16355 // (2a1f907) and
16356 // `children_returns_children_slice_verbatim_across_permutations`
16357 // (c17b51e) pins opened and the M3
16358 // `membros_returns_membros_slice_verbatim_across_permutations`
16359 // (0f26987) pin folded on, closing the outer-`Caixa` M3 mesh-
16360 // slot arm of the composite-slice sub-family. Peer at the outer
16361 // altitude of the closed inner-
16362 // [`crate::AplicacaoSpec::contratos`] (0dcc926) accessor on the
16363 // same MESH-COMPOSITION per-Aplicacao contract-list axis.
16364 let fixtures: Vec<Vec<crate::aplicacao::WitContract>> = vec![
16365 vec![],
16366 vec![contrato_http_for_test("cart", "catalog", "/items")],
16367 vec![
16368 contrato_http_for_test("cart", "catalog", "/items"),
16369 contrato_http_for_test("cart", "pricing", "/price"),
16370 contrato_http_for_test("cart", "auth", "/whoami"),
16371 ],
16372 ];
16373 for contratos in fixtures {
16374 let c = caixa_aplicacao_with_contratos(contratos.clone());
16375 assert_eq!(
16376 c.contratos(),
16377 contratos.as_slice(),
16378 "Caixa::contratos must return :contratos verbatim \
16379 (got {:?}, expected {contratos:?})",
16380 c.contratos(),
16381 );
16382 assert_eq!(
16383 c.contratos(),
16384 c.contratos.as_slice(),
16385 "Caixa::contratos must element-equal the raw \
16386 `self.contratos.as_slice()` field access across every \
16387 value in the Vec<WitContract> accept-set",
16388 );
16389 assert_eq!(
16390 c.contratos().is_empty(),
16391 c.contratos.is_empty(),
16392 "Caixa::contratos().is_empty() must byte-equal \
16393 self.contratos.is_empty() — a presence-bit drift would \
16394 silently split the paired Caixa::declared_mesh_slots \
16395 mesh declared-slot enumerator's presence probe from \
16396 the peer Caixa::aplicacao_view typed-view composer's \
16397 fold-in path",
16398 );
16399 }
16400 }
16401
16402 #[test]
16403 fn declared_mesh_slots_contratos_arm_routes_through_accessor() {
16404 // Composition pin: [`Caixa::declared_mesh_slots`]'s `:contratos`
16405 // presence-probe arm must key off [`Caixa::contratos`], not the
16406 // raw `!self.contratos.is_empty()` field-probe. Structurally: a
16407 // `Caixa { contratos: vec![WitContract { .. }], .. }` must push
16408 // `M3_AUTHOR_KEY_CONTRATOS` onto the declared-slot list (the
16409 // presence bit is non-empty, so the mesh kind-coherence gate
16410 // must surface the slot as "declared"), and a `Caixa {
16411 // contratos: vec![], .. }` must NOT push the label (the "author
16412 // omitted the slot entirely" arm — the empty-slice partition
16413 // the serde-default folds onto). The pair jointly pins the
16414 // accessor + declared-slot enumerator composition: any future
16415 // silent detour that had the accessor collapse
16416 // `[WitContract { .. }]` to `[]` (a `.filter(|c| c.de() !=
16417 // "__reserved__")` projection) would silently absorb the
16418 // "declared but degenerate" arm at the accessor boundary and
16419 // the [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
16420 // coherence gate would silently accept a struct-literal
16421 // `Caixa` carrying the drift.
16422 //
16423 // Peer of the sibling
16424 // `declared_servico_slots_upgrade_from_arm_routes_through_accessor`
16425 // (2a1f907),
16426 // `declared_supervisor_slots_children_arm_routes_through_accessor`
16427 // (c17b51e), and
16428 // `declared_mesh_slots_membros_arm_routes_through_accessor`
16429 // (0f26987) composition pins on the M2 `:upgrade-from` /
16430 // `:children` / M3 `:membros` composite-slice arms — same "the
16431 // enumerator gate must route through the substrate-primitive
16432 // typed dispatch" discipline extended onto the M3 `:contratos`
16433 // composite-slice arm, closing the M3 mesh-slot arm of the
16434 // declared-slot enumerator's routing invariant on the
16435 // composite-slice inputs.
16436 let c = caixa_aplicacao_with_contratos(vec![contrato_http_for_test(
16437 "cart", "catalog", "/items",
16438 )]);
16439 let slots = c.declared_mesh_slots();
16440 assert!(
16441 slots.contains(&crate::render::M3_AUTHOR_KEY_CONTRATOS),
16442 "declared_mesh_slots must push M3_AUTHOR_KEY_CONTRATOS when \
16443 `:contratos` is non-empty — the accessor and the enumerator \
16444 gate must route through the same substrate-primitive \
16445 typed dispatch on the outer :contratos presence bit (got \
16446 slots={slots:?})",
16447 );
16448 let c = caixa_aplicacao_with_contratos(vec![]);
16449 let slots = c.declared_mesh_slots();
16450 assert!(
16451 !slots.contains(&crate::render::M3_AUTHOR_KEY_CONTRATOS),
16452 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_CONTRATOS \
16453 when `:contratos` is empty — the author-omitted arm must \
16454 route through the accessor's empty-slice return unchanged \
16455 (got slots={slots:?})",
16456 );
16457 }
16458
16459 #[test]
16460 fn aplicacao_view_contratos_arm_routes_through_accessor() {
16461 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:contratos`
16462 // fold-in arm must key off [`Caixa::contratos`], not the raw
16463 // `self.contratos.clone()` field-clone. Structurally: a `Caixa
16464 // { kind: Aplicacao, contratos: vec![WitContract { de: "cart",
16465 // .. }, WitContract { de: "pricing", .. }], .. }` must fold the
16466 // per-edge list through the accessor into the typed
16467 // [`crate::AplicacaoSpec`] view's `contratos` slot verbatim —
16468 // every entry the accessor surfaces must land in the view's
16469 // `contratos` slot in the same order. The pair jointly pins
16470 // the accessor + view-composer composition: a future silent
16471 // detour that had the accessor shuffle or drop an edge would
16472 // silently split the paired declared-slot enumerator's
16473 // presence bit from the typed-view composer's edge-list, a
16474 // two-consumer split at the enumerator and the view composer
16475 // far from the source `caixa.lisp`.
16476 //
16477 // Peer of the sibling
16478 // `aplicacao_view_membros_arm_routes_through_accessor`
16479 // (0f26987) composition pin on the M3 `:membros` outer-
16480 // `&[Composite]` composite-slice arm, closing the aplicacao-
16481 // view composer's routing invariant on the composite-slice
16482 // inputs at the outer altitude.
16483 let c = caixa_aplicacao_with_contratos(vec![
16484 contrato_http_for_test("cart", "catalog", "/items"),
16485 contrato_http_for_test("cart", "pricing", "/price"),
16486 ]);
16487 let view = c
16488 .aplicacao_view()
16489 .expect("Aplicacao kind must produce an aplicacao_view");
16490 assert_eq!(
16491 view.contratos(),
16492 c.contratos(),
16493 "aplicacao_view must fold Caixa::contratos verbatim into \
16494 AplicacaoSpec::contratos — the accessor and the view \
16495 composer must route through the same substrate-primitive \
16496 typed dispatch on the outer :contratos slice (got view \
16497 contratos={:?}, expected {:?})",
16498 view.contratos(),
16499 c.contratos(),
16500 );
16501 }
16502
16503 #[test]
16504 fn contratos_projects_slice_by_borrow() {
16505 // The by-borrow pin: [`Caixa::contratos`] returns `&[WitContract]`
16506 // by borrow — the returned slice borrows the underlying
16507 // `Vec<WitContract>` storage of the `:contratos` slot and the
16508 // accessor must not clone the backing `Vec` on every call.
16509 // Peer of the sibling outer top-level [`Caixa`] `&[T]`-return
16510 // by-borrow pins (`autores_projects_slice_by_borrow` b5d813f,
16511 // `etiquetas_projects_slice_by_borrow` 78c7d3c,
16512 // `bibliotecas_projects_slice_by_borrow` 8a36c23,
16513 // `exe_projects_slice_by_borrow` 65d9527,
16514 // `servicos_projects_slice_by_borrow` 611f78b,
16515 // `deps_projects_slice_by_borrow` ad34b4e,
16516 // `deps_dev_projects_slice_by_borrow` f7fd81e,
16517 // `upgrade_from_projects_slice_by_borrow` 2a1f907,
16518 // `children_projects_slice_by_borrow` c17b51e,
16519 // `membros_projects_slice_by_borrow` 0f26987) on the sibling
16520 // outer top-level [`Caixa`] scalar-element and composite-
16521 // element `&[T]` axes — closes the outer-`Caixa` M3 mesh-slot
16522 // composite-element `&[Composite]` axis on the by-borrow pin:
16523 // the accessor's returned slice must borrow from `&self` (the
16524 // returned reference's lifetime is tied to `&self`), and
16525 // calling the accessor twice on the same [`Caixa`] must yield
16526 // slices that are pointer-equal (the underlying byte-buffer is
16527 // the storage `Vec`'s allocation, not a fresh copy) as well as
16528 // value-equal (idempotent, no side effects on `&self`).
16529 //
16530 // Pins against a future silent detour that returned an owned
16531 // `Vec<WitContract>` (which would type-check but silently clone
16532 // on every call), a `&Vec<WitContract>` return (which would
16533 // leak the backing `Vec`'s grow/push/reserve surface no
16534 // downstream consumer reaches for), or a one-arm-only accessor
16535 // that returned a saturating value on some sentinel input.
16536 for contratos in [
16537 vec![],
16538 vec![contrato_http_for_test("cart", "catalog", "/items")],
16539 vec![
16540 contrato_http_for_test("cart", "catalog", "/items"),
16541 contrato_http_for_test("cart", "pricing", "/price"),
16542 ],
16543 ] {
16544 let c = caixa_aplicacao_with_contratos(contratos.clone());
16545 let first = c.contratos();
16546 let second = c.contratos();
16547 assert_eq!(
16548 first, second,
16549 "Caixa::contratos must be idempotent — two successive \
16550 calls on the same &self must return the same \
16551 &[WitContract]",
16552 );
16553 assert_eq!(
16554 first.as_ptr(),
16555 second.as_ptr(),
16556 "Caixa::contratos must borrow the underlying \
16557 Vec<WitContract> storage — two successive calls must \
16558 return slices with the same backing pointer (a fresh \
16559 Vec<WitContract> clone would change the pointer on \
16560 every call)",
16561 );
16562 assert_eq!(
16563 first,
16564 contratos.as_slice(),
16565 "Caixa::contratos must return :contratos verbatim by \
16566 borrow — got {first:?}, expected {contratos:?}",
16567 );
16568 }
16569 }
16570
16571 // ── drift-detection: Caixa top-level multi-word serde-derive-to-const identity ──
16572
16573 #[test]
16574 fn caixa_multi_word_serde_keys_match_lifted_top_level_key_consts() {
16575 // Load-bearing invariant: every multi-word top-level [`Caixa`]
16576 // serde-derived JSON key routes through a lifted `&'static str`
16577 // const. The Rust field names are `snake_case`
16578 // (`deps_dev` / `upgrade_from` / `max_restarts` /
16579 // `restart_window`); [`Caixa`]'s `#[serde(rename_all =
16580 // "camelCase")]` derive attribute maps each to the camelCase
16581 // byte-string the [`Caixa::to_lisp`] round-trip's
16582 // `serde_json::to_value(self)` step lands under before
16583 // `tatara_lisp::domain::json_to_sexp` re-projects the JSON keys
16584 // to the kebab-case `:deps-dev` / `:upgrade-from` /
16585 // `:max-restarts` / `:restart-window` author surface. Serialize
16586 // a fully-populated [`Caixa`] and pin that each canonical
16587 // byte-sequence appears verbatim in the JSON — a future
16588 // accidental `rename_all = "snake_case"` / `"kebab-case"` /
16589 // verbatim-field-name flip at the derive attribute (any of
16590 // which would silently break every [`Caixa::to_lisp`]
16591 // round-trip and the future M4 operator-side manifest ingest's
16592 // `Value::get(<key>)` navigation) surfaces here as a build-time
16593 // test failure at `manifest.rs`, not as an apply-time
16594 // `.get(<stale-canonical-const>)` returning `None` far from the
16595 // derive-attr drift's commit. Same discipline the sibling
16596 // `supervisor_spec_serde_keys_match_lifted_supervisor_key_consts`
16597 // (40cc4e5), `membro_serde_keys_match_lifted_membro_key_consts`
16598 // (ce80ca0), and `upgrade_from_entry_serde_keys_match_lifted_
16599 // m2_upgrade_from_key_consts` (36ffe65) pins established on the
16600 // sibling M2 supervision-tree, M3 [`Membro`] per-entry, and M2
16601 // [`UpgradeFromEntry`] per-entry axes — extended here to the
16602 // enclosing M0 [`Caixa`] top-level axis so the last of the four
16603 // multi-word top-level [`Caixa`] serde-derived JSON keys
16604 // (`depsDev`) joins the substrate's "one canonical byte-string
16605 // per typed serialized-key axis" discipline.
16606 use crate::supervisor::{ChildSpec, RestartPolicy, RestartStrategy};
16607 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
16608 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
16609 c.deps_dev = vec![Dep::simple("tatara-check", "^0.1")];
16610 c.upgrade_from = vec![UpgradeFromEntry {
16611 from: "0.0.1".into(),
16612 instructions: vec![UpgradeInstruction::Restart],
16613 }];
16614 c.estrategia = Some(RestartStrategy::OneForOne);
16615 c.max_restarts = Some(3);
16616 c.restart_window = Some("60s".into());
16617 c.children = vec![ChildSpec {
16618 caixa: "child".into(),
16619 versao: "^0.1".into(),
16620 restart: RestartPolicy::Permanent,
16621 }];
16622 let json = serde_json::to_string(&c).unwrap();
16623 for key in [
16624 crate::render::CAIXA_KEY_DEPS_DEV,
16625 crate::render::M2_KEY_UPGRADE_FROM,
16626 crate::render::SUPERVISOR_KEY_MAX_RESTARTS,
16627 crate::render::SUPERVISOR_KEY_RESTART_WINDOW,
16628 ] {
16629 let quoted = format!("\"{key}\"");
16630 assert!(
16631 json.contains("ed),
16632 "serialized Caixa must carry the lifted top-level \
16633 multi-word byte-sequence {quoted} verbatim in the JSON \
16634 emission (got: {json})",
16635 );
16636 }
16637 }
16638
16639 #[test]
16640 fn caixa_top_level_multi_word_key_consts_are_pairwise_distinct() {
16641 // Cross-axis drift-detection pin: a future collapse of the four
16642 // canonical [`Caixa`] top-level multi-word byte-strings onto the
16643 // same value (e.g. an accidental copy-paste flip of
16644 // [`crate::render::CAIXA_KEY_DEPS_DEV`] to also read
16645 // `"upgradeFrom"`) would silently reroute every downstream
16646 // `Value::get(<key>)` probe on one axis onto the sibling axis's
16647 // top-level entry and pass every propagation-probe test that
16648 // expected only the stale axis's value. Peer of the sibling
16649 // four-way distinct pin on the `SUPERVISOR_KEY_*` tetrad
16650 // (40cc4e5) and the two-way pin on `MEMBRO_KEY_*` (ce80ca0).
16651 let all = [
16652 crate::render::CAIXA_KEY_DEPS_DEV,
16653 crate::render::M2_KEY_UPGRADE_FROM,
16654 crate::render::SUPERVISOR_KEY_MAX_RESTARTS,
16655 crate::render::SUPERVISOR_KEY_RESTART_WINDOW,
16656 ];
16657 for (i, a) in all.iter().enumerate() {
16658 for b in all.iter().skip(i + 1) {
16659 assert_ne!(
16660 a, b,
16661 "Caixa top-level multi-word key consts must be \
16662 pairwise-distinct canonical byte-sequences — got \
16663 `{a}` == `{b}`",
16664 );
16665 }
16666 }
16667 }
16668
16669 #[test]
16670 fn caixa_top_level_multi_word_key_consts_are_lower_camel_case_shape() {
16671 // Shape-pin: every [`Caixa`] top-level multi-word key const must
16672 // be a lowerCamelCase byte-sequence (no `snake_case`
16673 // underscores, no `kebab-case` hyphens, no leading colon, no
16674 // `PascalCase` leading capital, no whitespace / dots) — the
16675 // canonical shape the `#[serde(rename_all = "camelCase")]`
16676 // derive produces on [`Caixa`]. A future flip to a
16677 // non-camelCase attribute at the derive surfaces both here
16678 // (this test fails on the stale-constant shape) and at
16679 // `caixa_multi_word_serde_keys_match_lifted_top_level_key_consts`
16680 // (that test fails on the mismatch between const and derive).
16681 // Peer with `membro_key_consts_are_lower_camel_case_shape`
16682 // (ce80ca0) and `supervisor_key_consts_are_lower_camel_case_shape`
16683 // (40cc4e5) on the sibling per-entry / supervisor-tree axes.
16684 for key in [
16685 crate::render::CAIXA_KEY_DEPS_DEV,
16686 crate::render::M2_KEY_UPGRADE_FROM,
16687 crate::render::SUPERVISOR_KEY_MAX_RESTARTS,
16688 crate::render::SUPERVISOR_KEY_RESTART_WINDOW,
16689 ] {
16690 assert!(
16691 !key.is_empty(),
16692 "Caixa top-level multi-word key const must be non-empty \
16693 (got {key:?})"
16694 );
16695 let first = key.chars().next().unwrap();
16696 assert!(
16697 first.is_ascii_lowercase(),
16698 "Caixa top-level multi-word key const must lead with an \
16699 ASCII-lowercase byte (got {key:?}, leads with {first:?})",
16700 );
16701 assert!(
16702 key.chars().all(|c| c.is_ascii_alphanumeric()),
16703 "Caixa top-level multi-word key const must be \
16704 ASCII-alphanumeric only — no `_` / `-` / `:` / `.` / \
16705 whitespace (got {key:?})",
16706 );
16707 }
16708 }
16709
16710 #[test]
16711 fn caixa_key_deps_dev_pins_canonical_camel_case_byte_string() {
16712 // Scalar-value pin: the byte-string the
16713 // [`crate::render::CAIXA_KEY_DEPS_DEV`] const resolves to,
16714 // asserted verbatim. A future rebrand (`depsDev` → `devDeps`
16715 // matching Cargo's verbatim `dev-dependencies` axis, `depsDev`
16716 // → `depsTest` matching a hypothetical per-test-target
16717 // vocabulary flip) lands as an edit to exactly one const AND
16718 // one derive attribute — the sibling
16719 // `caixa_multi_word_serde_keys_match_lifted_top_level_key_consts`
16720 // pin already ties the const to the derive attribute, so a
16721 // rebrand that touches only one side of the pair fails at
16722 // caixa-core build time. Same "scalar-value pin per const"
16723 // discipline the sibling
16724 // `m2_top_level_author_key_consts_pin_canonical_kebab_case_labels`
16725 // (f49c8b0) and `contrato_key_consts_pin_canonical_camel_case_labels`
16726 // (ca463a4) pins carry on the peer M2 / M3 top-level slot axes.
16727 assert_eq!(crate::render::CAIXA_KEY_DEPS_DEV, "depsDev");
16728 }
16729
16730 #[test]
16731 fn caixa_key_deps_pins_canonical_byte_string() {
16732 // Scalar-value pin: the byte-string the
16733 // [`crate::render::CAIXA_KEY_DEPS`] const resolves to, asserted
16734 // verbatim. Peer of `caixa_key_deps_dev_pins_canonical_camel_case_byte_string`
16735 // on the two-list dep-graph serialized-key axis — the sibling
16736 // pin covers the multi-word `deps_dev → depsDev` camelCase
16737 // arm, this pin covers the single-word `deps → deps` no-op arm
16738 // (the [`crate::Caixa::deps`] field name carries no `_`, so the
16739 // `#[serde(rename_all = "camelCase")]` derive is a no-op on this
16740 // axis and the emitted JSON key equals the source-side field
16741 // name byte-for-byte). A future [`crate::Caixa::deps`] field
16742 // rename (`deps` → `dependencies` matching Cargo's verbatim
16743 // `[dependencies]` axis, `deps` → `runtime_deps` matching a
16744 // hypothetical per-runtime-target vocabulary flip) OR an added
16745 // `#[serde(rename = "…")]` explicit override lands as an edit
16746 // to exactly one const AND one derive-attr / field name — the
16747 // sibling `caixa_deps_serde_key_matches_lifted_caixa_key_deps`
16748 // pin ties the const to the emitted JSON key, so a rebrand
16749 // that touches only one side of the pair fails at caixa-core
16750 // build time.
16751 assert_eq!(crate::render::CAIXA_KEY_DEPS, "deps");
16752 }
16753
16754 #[test]
16755 fn caixa_deps_serde_key_matches_lifted_caixa_key_deps() {
16756 // Load-bearing invariant on the single-word `deps` top-level
16757 // axis: the byte-string [`crate::render::CAIXA_KEY_DEPS`] pins
16758 // must appear verbatim in the JSON [`Caixa::to_lisp`]'s
16759 // `serde_json::to_value(self)` step emits. Serialize a
16760 // populated [`Caixa`] whose `:deps` slot carries at least one
16761 // entry (the `#[serde(default)]` attribute on the field emits
16762 // an empty `[]` even without members, but a non-empty vec
16763 // additionally covers the codec's per-`Dep`-entry emission
16764 // path) and pin that `"deps"` appears verbatim in the JSON
16765 // emission — a future accidental `rename_all = "snake_case"` /
16766 // `"kebab-case"` flip at the derive attribute (or an added
16767 // `#[serde(rename = "…")]` explicit override on the field, or
16768 // a Rust field rename) would break every [`Caixa::to_lisp`]
16769 // round-trip and the future M4 operator-side manifest ingest's
16770 // `Value::get(CAIXA_KEY_DEPS)` navigation — surfaces here as a
16771 // build-time test failure at `manifest.rs`, not as an
16772 // apply-time `.get(<stale-canonical-const>)` returning `None`
16773 // far from the drift's commit. Peer of the sibling
16774 // `caixa_multi_word_serde_keys_match_lifted_top_level_key_consts`
16775 // multi-word pin on the same M0 [`Caixa`] top-level
16776 // serialized-key axis, extended here to the single-word arm
16777 // the multi-word test's `rename_all = "camelCase"` sweep can't
16778 // reach (single-word `deps → deps` is a no-op the multi-word
16779 // pin's `\"depsDev\"` / `\"upgradeFrom\"` / `\"maxRestarts\"` /
16780 // `\"restartWindow\"` byte-scan can never observe).
16781 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
16782 c.deps = vec![Dep::simple("caixa-core", "^0.1")];
16783 let json = serde_json::to_string(&c).unwrap();
16784 let quoted = format!("\"{}\"", crate::render::CAIXA_KEY_DEPS);
16785 assert!(
16786 json.contains("ed),
16787 "serialized Caixa must carry the lifted top-level `deps` \
16788 byte-sequence {quoted} verbatim in the JSON emission (got: \
16789 {json})",
16790 );
16791 }
16792
16793 #[test]
16794 fn caixa_dep_graph_two_list_key_consts_are_pairwise_distinct() {
16795 // Cross-axis drift-detection pin on the two-list dep-graph
16796 // renderer-side wire-key axis: a future collapse of the
16797 // canonical [`crate::render::CAIXA_KEY_DEPS`] /
16798 // [`crate::render::CAIXA_KEY_DEPS_DEV`] byte-strings onto the
16799 // same value (e.g. an accidental copy-paste flip of
16800 // `CAIXA_KEY_DEPS_DEV` to also read `"deps"`) would silently
16801 // reroute every downstream `Value::get(<key>)` probe on one
16802 // axis onto the sibling axis's dep-list and pass every
16803 // propagation-probe test that expected only the stale axis's
16804 // value — a dev-only dep would land in the runtime closure at
16805 // publish time, or a runtime dep would be excluded from the
16806 // published lacre. Peer of the sibling four-way distinct pin
16807 // on the top-level multi-word tetrad
16808 // (`caixa_top_level_multi_word_key_consts_are_pairwise_distinct`)
16809 // and the two-way pin on the sibling
16810 // [`DEP_AUTHOR_KEY_DEPS`] / [`DEP_AUTHOR_KEY_DEPS_DEV`]
16811 // author-facing arm (4da6fba's test), extended here to the
16812 // renderer-side wire-key arm of the same two-list dep-graph
16813 // axis so both halves of the "one canonical byte-string per
16814 // typed axis per (author, wire)" grid carry the same
16815 // distinct-ness discipline.
16816 assert_ne!(
16817 crate::render::CAIXA_KEY_DEPS,
16818 crate::render::CAIXA_KEY_DEPS_DEV,
16819 "CAIXA_KEY_DEPS and CAIXA_KEY_DEPS_DEV must be distinct \
16820 canonical byte-sequences on the two-list dep-graph \
16821 renderer-side wire-key axis"
16822 );
16823 }
16824
16825 // ── DepList / Caixa::push_dep pin ────────────────────────────────
16826 //
16827 // The compounding pin: the two-arm closed-set typed enum
16828 // [`crate::dep::DepList`] carries the runtime-closure `:deps`
16829 // (`Prod`) vs dev-only-closure `:deps-dev` (`Dev`) dispatch every
16830 // consumer of the top-level manifest's dep-mutation surface reads
16831 // through, and the typed dispatch [`Caixa::push_dep`] on the
16832 // substrate primitive folds the "select list → check within-list
16833 // dup → push" cascade onto one method call. Prior to this landing
16834 // the two axes lived across two `&'static str` constants
16835 // (`DEP_AUTHOR_KEY_DEPS`, `DEP_AUTHOR_KEY_DEPS_DEV`) with no closed-
16836 // set type carrying the pair; the `feira add` mutation site's
16837 // inline `if self.dev { &mut caixa.deps_dev } else { &mut
16838 // caixa.deps }` dispatch expressed no compile-time link back to
16839 // the substrate primitive, and a future third dep-list axis would
16840 // have silently split at every open-coded mutation site.
16841
16842 #[test]
16843 fn dep_list_as_str_routes_through_lifted_author_key_constants() {
16844 // Every arm returns the same `&'static str` the substrate's
16845 // canonical `DEP_AUTHOR_KEY_DEPS` / `DEP_AUTHOR_KEY_DEPS_DEV`
16846 // constants carry. A future rebrand on either constant reaches
16847 // the enum through one edit; a regression to inline literals
16848 // (e.g. `Prod => ":deps"`) would silently split the diagnostic
16849 // quotes from the wire-format constants every consumer routes
16850 // through and this pin flags it at build time.
16851 assert_eq!(
16852 crate::dep::DepList::Prod.as_str(),
16853 crate::render::DEP_AUTHOR_KEY_DEPS
16854 );
16855 assert_eq!(
16856 crate::dep::DepList::Dev.as_str(),
16857 crate::render::DEP_AUTHOR_KEY_DEPS_DEV
16858 );
16859 }
16860
16861 #[test]
16862 fn dep_list_display_routes_through_as_str() {
16863 // Same as-str-through-Display convergence discipline the
16864 // sibling closed-set typed enums carry — a `format!("{list}")`
16865 // call must land byte-for-byte on the accessor's return so a
16866 // future consumer that formats the enum for a diagnostic line
16867 // reaches the same wire-format constant the wire-format
16868 // producers do.
16869 assert_eq!(
16870 format!("{}", crate::dep::DepList::Prod),
16871 crate::dep::DepList::Prod.as_str()
16872 );
16873 assert_eq!(
16874 format!("{}", crate::dep::DepList::Dev),
16875 crate::dep::DepList::Dev.as_str()
16876 );
16877 }
16878
16879 #[test]
16880 fn dep_list_all_enumerates_every_variant_once() {
16881 // Exhaustive-iteration pin — every arm appears exactly once in
16882 // `ALL`, matching the closed set the compiler enforces on the
16883 // sibling `match self` arms. A future variant addition that
16884 // extends only one method's match without extending `ALL`
16885 // would silently drop the new arm from every consumer that
16886 // iterates the slice.
16887 let variants: &[crate::dep::DepList] = crate::dep::DepList::ALL;
16888 assert!(variants.contains(&crate::dep::DepList::Prod));
16889 assert!(variants.contains(&crate::dep::DepList::Dev));
16890 assert_eq!(variants.len(), 2);
16891 }
16892
16893 #[test]
16894 fn dep_list_from_wire_returns_prod_on_deps_wire_scalar() {
16895 // Reverse projection on the two-list dep-graph axis: the
16896 // author-surface wire tag the sibling `as_str` emitter walks
16897 // for `Prod` (`:deps` via `DEP_AUTHOR_KEY_DEPS`) parses back to
16898 // `Some(DepList::Prod)`. A regression that hand-rolled the
16899 // per-arm match without routing through the lifted
16900 // `DEP_AUTHOR_KEY_DEPS` const would silently disagree on any
16901 // future wire-tag rebrand and this pin flags it at build time.
16902 assert_eq!(
16903 crate::dep::DepList::from_wire(crate::render::DEP_AUTHOR_KEY_DEPS),
16904 Some(crate::dep::DepList::Prod)
16905 );
16906 }
16907
16908 #[test]
16909 fn dep_list_from_wire_returns_dev_on_deps_dev_wire_scalar() {
16910 // Peer of the `Prod`-arm pin on the dev-only axis: the
16911 // author-surface wire tag the sibling `as_str` emitter walks
16912 // for `Dev` (`:deps-dev` via `DEP_AUTHOR_KEY_DEPS_DEV`) parses
16913 // back to `Some(DepList::Dev)`. Same drift-detection posture
16914 // as the peer arm — the sibling method `match` arms are
16915 // compiler-checked exhaustive so a future variant addition
16916 // trips at build time.
16917 assert_eq!(
16918 crate::dep::DepList::from_wire(crate::render::DEP_AUTHOR_KEY_DEPS_DEV),
16919 Some(crate::dep::DepList::Dev)
16920 );
16921 }
16922
16923 #[test]
16924 fn dep_list_from_wire_returns_none_on_unknown_wire_scalar() {
16925 // Every input outside the closed-set arm-string set the
16926 // sibling `as_str` emitter walks lands on the terminal `None`
16927 // fallback — no silent-accept surface. Sweeps a set of
16928 // plausibly-adjacent scalars (unprefixed wire form, PascalCase
16929 // rebrand candidates, foreign wire tags, empty string) so a
16930 // future variant addition that widened one wire form without
16931 // extending the emitter's arm-set would trip the sibling
16932 // round-trip pin below rather than silently accepting the new
16933 // form here.
16934 for candidate in [
16935 "",
16936 "deps",
16937 "deps-dev",
16938 ":deps ",
16939 ":Deps",
16940 ":DEPS",
16941 ":build-dep",
16942 ":tool-dep",
16943 "prod",
16944 "dev",
16945 ] {
16946 assert_eq!(
16947 crate::dep::DepList::from_wire(candidate),
16948 None,
16949 "from_wire({candidate:?}) must return None; every input outside \
16950 the {{DEP_AUTHOR_KEY_DEPS, DEP_AUTHOR_KEY_DEPS_DEV}} accept-set \
16951 the sibling as_str emitter walks lands on the terminal fallback",
16952 );
16953 }
16954 }
16955
16956 #[test]
16957 fn dep_list_round_trips_through_as_str_and_from_wire() {
16958 // Load-bearing round-trip pin: every arm the `ALL` iteration
16959 // exposes survives the `as_str` → `from_wire` composition
16960 // byte-for-byte. Same discipline the sibling closed-set enums
16961 // carry — `CaixaKind` /
16962 // `RestartStrategy` / `RestartPolicy` /
16963 // `PlacementStrategy` — extended onto the two-list dep-graph
16964 // axis. A future variant addition that extends `ALL` +
16965 // `as_str` without extending `from_wire` (or vice versa)
16966 // trips at build time on this iteration because the compiler
16967 // enforces exhaustiveness on the sibling `match self` arms.
16968 for &list in crate::dep::DepList::ALL {
16969 assert_eq!(
16970 crate::dep::DepList::from_wire(list.as_str()),
16971 Some(list),
16972 "DepList::from_wire(as_str({list:?})) must round-trip to Some({list:?}) — \
16973 a silent split between the forward emitter and the reverse parser \
16974 would drift the two halves of the two-list dep-graph axis's typed dispatch",
16975 );
16976 }
16977 }
16978
16979 #[test]
16980 fn push_dep_routes_to_deps_slot_on_prod_arm() {
16981 // The `Prod` arm dispatches to the runtime-closure `:deps`
16982 // slot every downstream lacre-pipeline consumer resolves at
16983 // build time. A future arm that regressed to inline `&mut
16984 // self.deps_dev` on the `Prod` path would silently reroute
16985 // every runtime dep into the dev-only closure at publish time
16986 // — this pin refuses that regression.
16987 let src = Caixa::template("host");
16988 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
16989 let before_deps = caixa.deps().len();
16990 let before_deps_dev = caixa.deps_dev().len();
16991 let dep = Dep {
16992 nome: "caixa-teia".to_string(),
16993 versao: "^0.1".to_string(),
16994 fonte: None,
16995 opcional: false,
16996 caracteristicas: Vec::new(),
16997 };
16998 caixa
16999 .push_dep(crate::dep::DepList::Prod, dep)
17000 .expect("first push into :deps succeeds");
17001 assert_eq!(caixa.deps().len(), before_deps + 1);
17002 assert_eq!(caixa.deps_dev().len(), before_deps_dev);
17003 assert_eq!(caixa.deps().last().unwrap().nome(), "caixa-teia");
17004 }
17005
17006 #[test]
17007 fn push_dep_routes_to_deps_dev_slot_on_dev_arm() {
17008 // Peer of the sibling `Prod`-arm dispatch pin — the `Dev` arm
17009 // must dispatch to the dev-only-closure `:deps-dev` slot every
17010 // downstream test-facing artifact resolver reads. A future
17011 // regression that inverted the two arms would silently route
17012 // every dev-only dep into the runtime closure at publish time
17013 // and this pin catches it before the drift ships.
17014 let src = Caixa::template("host");
17015 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17016 let dep = Dep {
17017 nome: "tatara-check".to_string(),
17018 versao: "*".to_string(),
17019 fonte: None,
17020 opcional: false,
17021 caracteristicas: Vec::new(),
17022 };
17023 caixa
17024 .push_dep(crate::dep::DepList::Dev, dep)
17025 .expect("first push into :deps-dev succeeds");
17026 assert!(caixa.deps().is_empty());
17027 assert_eq!(caixa.deps_dev().len(), 1);
17028 assert_eq!(caixa.deps_dev().last().unwrap().nome(), "tatara-check");
17029 }
17030
17031 #[test]
17032 fn push_dep_refuses_within_list_duplicate_nome_with_typed_error() {
17033 // Within-list dup check routes through the canonical
17034 // [`DepError::DuplicateNome`] carrier — the substrate's typed
17035 // diagnostic for the same axis [`Caixa::validate_deps`]'s
17036 // parse-time [`crate::render::insert_first_seen`] walk raises
17037 // on. Prior to the lift the mutation site's inline
17038 // `bail!("dep '{}' already declared", …)` string-diagnostic
17039 // path expressed no through-line back to the typed error;
17040 // routing every dep-list refusal through one carrier means an
17041 // author reading a `feira add` refusal and a `feira build`
17042 // refusal reaches for the same corrective surface without
17043 // switching diagnostic idioms.
17044 let src = Caixa::template("host");
17045 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17046 let dep = Dep {
17047 nome: "caixa-teia".to_string(),
17048 versao: "^0.1".to_string(),
17049 fonte: None,
17050 opcional: false,
17051 caracteristicas: Vec::new(),
17052 };
17053 caixa
17054 .push_dep(crate::dep::DepList::Prod, dep.clone())
17055 .expect("first push succeeds");
17056 let dup = Dep {
17057 nome: "caixa-teia".to_string(),
17058 versao: "^0.2".to_string(),
17059 fonte: None,
17060 opcional: false,
17061 caracteristicas: Vec::new(),
17062 };
17063 let err = caixa
17064 .push_dep(crate::dep::DepList::Prod, dup)
17065 .expect_err("second push with same :nome refuses");
17066 assert_eq!(
17067 err,
17068 DepError::DuplicateNome {
17069 nome: "caixa-teia".to_string(),
17070 list: crate::render::DEP_AUTHOR_KEY_DEPS,
17071 }
17072 );
17073 // The refused mutation must not corrupt the target list —
17074 // exactly one entry lives past the refusal, matching the
17075 // canonical single-source-of-truth invariant `Caixa::deps()`
17076 // carries.
17077 assert_eq!(caixa.deps().len(), 1);
17078 }
17079
17080 #[test]
17081 fn push_dep_refuses_dup_on_dev_list_arm_names_deps_dev_key() {
17082 // Peer of the sibling `Prod`-arm dup-refusal pin — the `Dev`
17083 // arm's refusal must carry `DEP_AUTHOR_KEY_DEPS_DEV` in the
17084 // `list` payload so a future author reading the refusal grep's
17085 // for the correct `:deps-dev` block in their `caixa.lisp`,
17086 // not the sibling `:deps` block the runtime closure resolves.
17087 let src = Caixa::template("host");
17088 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17089 let dep = Dep {
17090 nome: "tatara-check".to_string(),
17091 versao: "*".to_string(),
17092 fonte: None,
17093 opcional: false,
17094 caracteristicas: Vec::new(),
17095 };
17096 caixa
17097 .push_dep(crate::dep::DepList::Dev, dep.clone())
17098 .expect("first push succeeds");
17099 let err = caixa
17100 .push_dep(crate::dep::DepList::Dev, dep)
17101 .expect_err("second push with same :nome refuses");
17102 assert!(matches!(
17103 err,
17104 DepError::DuplicateNome {
17105 ref nome,
17106 list,
17107 } if nome == "tatara-check"
17108 && list == crate::render::DEP_AUTHOR_KEY_DEPS_DEV
17109 ));
17110 }
17111
17112 #[test]
17113 fn push_dep_allows_same_nome_across_prod_and_dev_lists() {
17114 // The within-list dup check is scoped to the target arm — a
17115 // caixa may legitimately carry the same `:nome` under both
17116 // `:deps` and `:deps-dev` (though the substrate's peer
17117 // [`crate::Caixa::validate_deps`] walk still refuses the
17118 // shape at parse time; the mutation-site refusal is scoped to
17119 // the mutation-site's list to match the peer parse-time
17120 // per-list [`crate::render::insert_first_seen`] discipline).
17121 // The two arms hold independent seen-sets.
17122 let src = Caixa::template("host");
17123 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17124 let dep_prod = Dep {
17125 nome: "shared".to_string(),
17126 versao: "^0.1".to_string(),
17127 fonte: None,
17128 opcional: false,
17129 caracteristicas: Vec::new(),
17130 };
17131 let dep_dev = Dep {
17132 nome: "shared".to_string(),
17133 versao: "*".to_string(),
17134 fonte: None,
17135 opcional: false,
17136 caracteristicas: Vec::new(),
17137 };
17138 caixa
17139 .push_dep(crate::dep::DepList::Prod, dep_prod)
17140 .expect("push into :deps succeeds");
17141 caixa
17142 .push_dep(crate::dep::DepList::Dev, dep_dev)
17143 .expect("push same :nome into :deps-dev succeeds");
17144 assert_eq!(caixa.deps().len(), 1);
17145 assert_eq!(caixa.deps_dev().len(), 1);
17146 }
17147
17148 #[test]
17149 fn deps_of_prod_returns_the_deps_slot_verbatim() {
17150 // The `Prod` arm of the typed-dispatch [`Caixa::deps_of`] read
17151 // accessor must project onto the runtime-closure `:deps` slot —
17152 // element-equal and length-equal to the sibling per-slot
17153 // [`Caixa::deps`] accessor's return over every per-caixa fixture.
17154 // A future arm that regressed to `self.deps_dev()` on the `Prod`
17155 // path would silently reroute every downstream typed-dispatch
17156 // walker (the [`Caixa::validate_deps`] per-list
17157 // [`crate::render::insert_first_seen`] dedup walk, any future
17158 // per-axis-parametrised consumer) into the sibling dev-only
17159 // closure and this pin refuses that regression.
17160 let src = Caixa::template("host");
17161 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17162 assert_eq!(caixa.deps_of(crate::dep::DepList::Prod), caixa.deps());
17163 assert_eq!(caixa.deps_of(crate::dep::DepList::Prod).len(), 0);
17164 let dep = Dep {
17165 nome: "caixa-teia".to_string(),
17166 versao: "^0.1".to_string(),
17167 fonte: None,
17168 opcional: false,
17169 caracteristicas: Vec::new(),
17170 };
17171 caixa
17172 .push_dep(crate::dep::DepList::Prod, dep.clone())
17173 .expect("push into :deps succeeds");
17174 assert_eq!(caixa.deps_of(crate::dep::DepList::Prod), caixa.deps());
17175 assert_eq!(caixa.deps_of(crate::dep::DepList::Prod).len(), 1);
17176 assert_eq!(
17177 caixa.deps_of(crate::dep::DepList::Prod)[0].nome(),
17178 "caixa-teia"
17179 );
17180 }
17181
17182 #[test]
17183 fn deps_of_dev_returns_the_deps_dev_slot_verbatim() {
17184 // Peer of the sibling `Prod`-arm pin — the `Dev` arm of
17185 // [`Caixa::deps_of`] must project onto the dev-only-closure
17186 // `:deps-dev` slot, element-equal and length-equal to the
17187 // sibling per-slot [`Caixa::deps_dev`] accessor's return. A
17188 // future regression that inverted the two arms would silently
17189 // route every dev-list walker onto the runtime closure and this
17190 // pin catches it before the drift ships.
17191 let src = Caixa::template("host");
17192 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17193 assert_eq!(caixa.deps_of(crate::dep::DepList::Dev), caixa.deps_dev());
17194 assert_eq!(caixa.deps_of(crate::dep::DepList::Dev).len(), 0);
17195 let dep = Dep {
17196 nome: "tatara-check".to_string(),
17197 versao: "*".to_string(),
17198 fonte: None,
17199 opcional: false,
17200 caracteristicas: Vec::new(),
17201 };
17202 caixa
17203 .push_dep(crate::dep::DepList::Dev, dep)
17204 .expect("push into :deps-dev succeeds");
17205 assert_eq!(caixa.deps_of(crate::dep::DepList::Dev), caixa.deps_dev());
17206 assert_eq!(caixa.deps_of(crate::dep::DepList::Dev).len(), 1);
17207 assert_eq!(
17208 caixa.deps_of(crate::dep::DepList::Dev)[0].nome(),
17209 "tatara-check"
17210 );
17211 }
17212
17213 #[test]
17214 fn deps_of_exhaustive_over_dep_list_all_covers_the_two_slots() {
17215 // Composition pin: iterating [`crate::dep::DepList::ALL`] through
17216 // [`Caixa::deps_of`] must land on the same two-slot partition the
17217 // per-slot [`Caixa::deps`] / [`Caixa::deps_dev`] accessors
17218 // expose — the canonical dispatch a future per-axis-parametrised
17219 // walker (a future `feira app graph` per-list dep summary, a
17220 // future M4 per-cluster dev-closure-audit overlay the CR
17221 // materializer resolves per-CR) reads through. Prior to the
17222 // lift the two-block iteration lived open-coded at every walker,
17223 // so a future third dep-list axis (`:deps-build`, per CAIXA-SDLC
17224 // §I) would have had to grow a third block at every consumer.
17225 // A regression that dropped the `Dev` arm from `ALL` would flip
17226 // the collected pairs to `[(":deps", &[])]` alone and this pin
17227 // refuses that shape.
17228 let src = Caixa::template("host");
17229 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17230 let prod_dep = Dep {
17231 nome: "caixa-teia".to_string(),
17232 versao: "^0.1".to_string(),
17233 fonte: None,
17234 opcional: false,
17235 caracteristicas: Vec::new(),
17236 };
17237 let dev_dep = Dep {
17238 nome: "tatara-check".to_string(),
17239 versao: "*".to_string(),
17240 fonte: None,
17241 opcional: false,
17242 caracteristicas: Vec::new(),
17243 };
17244 caixa
17245 .push_dep(crate::dep::DepList::Prod, prod_dep)
17246 .expect("push into :deps succeeds");
17247 caixa
17248 .push_dep(crate::dep::DepList::Dev, dev_dep)
17249 .expect("push into :deps-dev succeeds");
17250 let collected: Vec<(&'static str, usize, &str)> = crate::dep::DepList::ALL
17251 .iter()
17252 .map(|&list| {
17253 let slice = caixa.deps_of(list);
17254 (list.as_str(), slice.len(), slice[0].nome())
17255 })
17256 .collect();
17257 assert_eq!(
17258 collected,
17259 vec![
17260 (crate::render::DEP_AUTHOR_KEY_DEPS, 1, "caixa-teia"),
17261 (crate::render::DEP_AUTHOR_KEY_DEPS_DEV, 1, "tatara-check"),
17262 ]
17263 );
17264 }
17265
17266 #[test]
17267 fn validate_deps_iterates_through_dep_list_all_via_deps_of() {
17268 // Composition pin: the [`Caixa::validate_deps`] parse-time gate
17269 // must route its per-list [`crate::render::insert_first_seen`]
17270 // dedup walk through [`Caixa::deps_of`] + [`crate::dep::DepList::ALL`]
17271 // rather than the pre-lift open-coded two-block iteration over
17272 // `self.deps()` + `self.deps_dev()`. A regression that dropped
17273 // one arm (e.g. hand-inlining `self.deps()` alone) would silently
17274 // stop refusing within-list dups on the sibling arm; a
17275 // regression that flipped the arm-to-list-key mapping
17276 // (`Dev => DEP_AUTHOR_KEY_DEPS`) would silently mislabel the
17277 // diagnostic surface. Both drifts surface here through a paired
17278 // duplicate-name refusal per arm plus an offending-list-key
17279 // check on the emitted [`DepError::DuplicateNome`] carrier.
17280 for &list in crate::dep::DepList::ALL {
17281 let src = Caixa::template("host");
17282 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17283 let dup = Dep {
17284 nome: "twin".to_string(),
17285 versao: "^0.1".to_string(),
17286 fonte: None,
17287 opcional: false,
17288 caracteristicas: Vec::new(),
17289 };
17290 match list {
17291 crate::dep::DepList::Prod => {
17292 caixa.deps.push(dup.clone());
17293 caixa.deps.push(dup);
17294 }
17295 crate::dep::DepList::Dev => {
17296 caixa.deps_dev.push(dup.clone());
17297 caixa.deps_dev.push(dup);
17298 }
17299 }
17300 let err = caixa
17301 .validate_deps()
17302 .expect_err("within-list duplicate :nome must refuse");
17303 assert_eq!(
17304 err,
17305 DepError::DuplicateNome {
17306 nome: "twin".to_string(),
17307 list: list.as_str(),
17308 },
17309 "validate_deps on {list} arm must emit \
17310 DepError::DuplicateNome carrying the arm's own \
17311 as_str() diagnostic — the arm-to-list-key mapping \
17312 flowed through DepList::ALL + Caixa::deps_of"
17313 );
17314 }
17315 }
17316}