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.
241 #[error(
242 "this is a `{palavra_canonica}` declaration ({descricao}), read by \
243 {consumidor} — not a caixa-core package manifest. `defcaixa` is the \
244 tatara-lisp package manifest (`:nome :versao :kind :deps …`); the two \
245 are different declarations that shared one keyword until 2026-07-31"
246 )]
247 DialetoEstrangeiro {
248 /// Which declaration this actually is.
249 dialeto: crate::dialeto::CaixaDialeto,
250 /// The keyword it should be written with.
251 palavra_canonica: &'static str,
252 /// Who reads it.
253 consumidor: &'static str,
254 /// One-line schema description.
255 descricao: &'static str,
256 },
257
258 /// Not a manifest declaration at all.
259 #[error(transparent)]
260 Dialeto(#[from] crate::dialeto::DialetoError),
261}
262
263impl Caixa {
264 /// Parse a `caixa.lisp` source string to a typed `Caixa`.
265 ///
266 /// Classifies the dialect **before** parsing. A `(defcaixa …)` of another
267 /// declaration is [`LeituraError::DialetoEstrangeiro`], naming what it is
268 /// and who reads it, instead of an unknown-keyword rejection that reads as
269 /// "your manifest is broken".
270 ///
271 /// The ordering is load-bearing. Handing a foreign dialect to the derive
272 /// first and interpreting the failure afterwards would mean guessing from
273 /// an error message, and the guess would be wrong for every file whose
274 /// first unknown slot happens to be one both schemas could plausibly carry.
275 pub fn from_lisp(src: &str) -> Result<Self, LeituraError> {
276 use tatara_lisp::domain::TataraDomain;
277 let forms = tatara_lisp::read(src).map_err(LeituraError::Leitura)?;
278 let first = forms.first().ok_or(crate::dialeto::DialetoError::Vazio)?;
279
280 match crate::dialeto::classify_form(first)? {
281 crate::dialeto::CaixaDialeto::Pacote => {}
282 // `Desconhecido` deliberately falls through to the derive rather
283 // than short-circuiting: a `(defcaixa …)` matching neither schema
284 // is most likely a genuine package manifest with a typo in
285 // `:nome`, and the derive's diagnostic — which names the offending
286 // keyword and suggests the nearest slot — is far better than
287 // anything this classifier could say.
288 crate::dialeto::CaixaDialeto::Desconhecido => {}
289 foreign => {
290 return Err(LeituraError::DialetoEstrangeiro {
291 dialeto: foreign,
292 palavra_canonica: foreign.palavra_canonica(),
293 consumidor: foreign.consumidor(),
294 descricao: foreign.descricao(),
295 });
296 }
297 }
298
299 Self::compile_from_sexp(first).map_err(LeituraError::Leitura)
300 }
301
302 /// Register `Caixa` with the global tatara-lisp domain registry so
303 /// `defcaixa` is dispatchable from any tatara-lisp binary that seeds
304 /// the registry (e.g. `tatara-check`).
305 ///
306 /// `pending-fallible-register`: upstream `tatara_lisp::domain::register`
307 /// became `-> Result<(), KeywordCollision>` on 2026-07-31, so a second type
308 /// claiming `defcaixa` in one process is refused and named instead of
309 /// silently displacing this one. This workspace pins
310 /// `tatara-lisp = "0.3.3"`, which predates that, so the result cannot be
311 /// checked here yet. Propagate it — `pub fn register() -> Result<(),
312 /// tatara_lisp::KeywordCollision>` — in the same commit that bumps the pin.
313 pub fn register() {
314 tatara_lisp::domain::register::<Self>();
315 }
316
317 /// Substrate-canonical per-`Caixa` `:licenca` SPDX-expression scalar
318 /// accessor every consumer of the top-level manifest's license axis
319 /// keys off — returns the author-declared `:licenca` byte-string
320 /// verbatim as an `Option<&str>`, borrowed from the typed slot's own
321 /// `Option<String>` storage. `None` when the slot is absent (the
322 /// canonical "omit to defer to the caixa-helm renderer's `MIT`
323 /// fallback" shape [`Self::validate_licenca`] documents at
324 /// caixa-core/src/manifest.rs:1560; the peer [`caixa-helm`]
325 /// `build_readme` fold at caixa-helm/src/lib.rs:962 reads this
326 /// predicate too, so an authored-but-unset `:licenca` round-trips to
327 /// a rendered `lareira-<nome>` chart's `README.md` `## License`
328 /// section structurally identical to one that omits the slot).
329 ///
330 /// The `:licenca` slot carries the universal-axis SPDX-expression
331 /// license identifier every kind of caixa emits under (CAIXA-SDLC
332 /// §I — the author-facing surface every `defcaixa` form supplies) —
333 /// the typed slot's `Option<String>` accept-set (empty-string
334 /// rejected through [`ManifestError::LicencaEmpty`], SPDX-alphabet-
335 /// invalid rejected through [`ManifestError::LicencaInvalid`]) maps
336 /// onto the `lareira-<nome>` Helm chart's `README.md` `## License`
337 /// section (caixa-helm/src/lib.rs:962) and (through future
338 /// tightening documented at [`Self::validate_licenca`]) the
339 /// Chart.yaml `annotations["artifacthub.io/license"]` axis every
340 /// registry-facing chart carries. Every downstream consumer that
341 /// reads the license byte-string keys off this scalar (the
342 /// [`Self::validate_licenca`] empty-arm + SPDX-shape gate that
343 /// routes through `self.licenca.as_deref()`, the caixa-helm
344 /// `build_readme` `unwrap_or_else(|| "MIT".into())` fold that keys
345 /// the fallback off the `Option::is_none()` arm, every future
346 /// per-`Caixa` registry-facing renderer the CAIXA-SDLC §I roadmap
347 /// acknowledges).
348 ///
349 /// Prior to this lift the `.licenca` field was accessed inline at
350 /// two production sites — [`Self::validate_licenca`]'s
351 /// `self.licenca.as_deref()` empty-and-shape gate binding and the
352 /// caixa-helm `build_readme` `caixa.licenca.clone().unwrap_or_else(||
353 /// "MIT".into())` `README.md` `## License` fold — two open-coded
354 /// field-accesses that expressed no compile-time link back to the
355 /// typed slot. A future extension of the `:licenca` axis to a
356 /// richer author surface — a per-`:licenca` structured SPDX
357 /// expression parser + license-id allowlist (the future tightening
358 /// [`Self::validate_licenca`]'s docstring acknowledges), a
359 /// per-cluster license-default overlay the M4 CR materializer
360 /// resolves per-CR (the "cluster policy pins `Apache-2.0` for every
361 /// unlisted caixa" arm), a promotion of the plain
362 /// `Option<String>` byte-string to a richer `SpdxExpression` enum
363 /// once the SPDX-expression parser lands — would have had to be
364 /// threaded through both open-coded copies in lockstep or the
365 /// validate gate and the caixa-helm emit path would silently
366 /// disagree on which license a given [`Caixa`] resolves to (an
367 /// author's `:licenca "MIT OR Apache-2.0"` would satisfy validate
368 /// while the emit path silently rendered a stale `MIT` fallback,
369 /// or vice versa). Lifting the resolution to a typed method on the
370 /// substrate primitive means every downstream consumer of the
371 /// caixa's per-`Caixa` license surface reaches for exactly one
372 /// typed dispatch — the resolver's accept-set migrates as a unit
373 /// on any future axis addition.
374 ///
375 /// First `Option<&str>`-return top-level [`Caixa`] scalar accessor —
376 /// opens the "outer [`Caixa`] `Option<&str>` scalar" projection
377 /// pattern the sibling per-`Caixa` `:descricao` / `:repositorio` /
378 /// `:edicao` future lifts fold on. Same "one typed dispatch on the
379 /// substrate primitive, thin projections at each consumer"
380 /// discipline the peer per-`:placement` [`crate::aplicacao::Placement::shard_key`]
381 /// (7cd2a28) / [`crate::aplicacao::Placement::affinity`] (74ec2d3)
382 /// / per-`:contratos` [`crate::aplicacao::WitContract::endpoint`]
383 /// (7020470) / [`crate::aplicacao::WitContract::subject`] (90de675)
384 /// / [`crate::aplicacao::WitContract::slot`] (ed22b66)
385 /// `Option<&str>`-return accessors carry on the sibling M2 / M3
386 /// typed-slot atom axes, extended here to the outer top-level
387 /// `Caixa` universal-axis surface. Named `licenca()` to match the
388 /// storage field's name; the accessor's identity maps onto the
389 /// canonical CAIXA-SDLC §I vocabulary the slot's docstring already
390 /// carries.
391 #[must_use]
392 pub fn licenca(&self) -> Option<&str> {
393 self.licenca.as_deref()
394 }
395
396 /// Substrate-canonical per-`Caixa` `:repositorio` git-repo-URL scalar
397 /// accessor every consumer of the top-level manifest's homepage /
398 /// source-of-truth axis keys off — returns the author-declared
399 /// `:repositorio` byte-string verbatim as an `Option<&str>`, borrowed
400 /// from the typed slot's own `Option<String>` storage. `None` when
401 /// the slot is absent (the canonical "omit to defer to the renderer's
402 /// per-target placeholder" shape — [`caixa-helm`]'s `ChartYaml.home`
403 /// carries the `Option<String>` through verbatim so an author-omitted
404 /// `:repositorio` renders a `Chart.yaml` without a `home:` field
405 /// (`skip_serializing_if = "Option::is_none"`), while [`caixa-flux`]'s
406 /// `ClusterBundleOpts::for_caixa` folds the omitted slot through a
407 /// `format!("https://github.com/{DEFAULT_PLEME_GIT_ORG}/{nome}")`
408 /// fallback derived from `caixa.nome`).
409 ///
410 /// The `:repositorio` slot carries the universal-axis git-repo-URL
411 /// homepage identifier every kind of caixa emits under (CAIXA-SDLC
412 /// §I — the author-facing surface every `defcaixa` form supplies) —
413 /// the typed slot's `Option<String>` accept-set (empty-string
414 /// rejected through [`ManifestError::RepositorioEmpty`], git-repo-URL-
415 /// shape-invalid rejected through [`ManifestError::RepositorioInvalid`]
416 /// past the shared [`crate::render::is_git_repo_url`] predicate the
417 /// peer per-`:deps :fonte :repo` axis also routes through) maps onto
418 /// four load-bearing downstream consumers:
419 ///
420 /// - [`Self::validate_repositorio`]'s empty-arm + shape-predicate
421 /// gate binding at caixa-core/src/manifest.rs:1456 — the
422 /// universal-axis identity gate wired at caixa-build time.
423 /// - [`caixa-helm`]'s `build_chart_yaml` `ChartYaml.home` fold at
424 /// caixa-helm/src/lib.rs:840 — the rendered `lareira-<nome>`
425 /// Helm chart's `Chart.yaml` `home:` field, which every registry
426 /// that ingests the chart (ArtifactHub, chartmuseum,
427 /// `helm search repo`) surfaces as the chart's canonical source-
428 /// of-truth link.
429 /// - [`caixa-helm`]'s `build_readme` `## Source` fold at
430 /// caixa-helm/src/lib.rs:957 — the rendered `lareira-<nome>`
431 /// chart's `README.md` header link back to the source repo,
432 /// which every author who inspects the rendered chart bundle
433 /// lands at.
434 /// - [`caixa-flux`]'s `ClusterBundleOpts::for_caixa`
435 /// `GitRepository.spec.url` fold at caixa-flux/src/lib.rs:2006 —
436 /// the rendered `GitRepository` CR's `spec.url` field, which
437 /// FluxCD's `source-controller` polls to reconcile the caixa's
438 /// manifest bundle from git.
439 ///
440 /// Prior to this lift the `.repositorio` field was accessed inline
441 /// at four production sites — [`Self::validate_repositorio`]'s
442 /// `self.repositorio.as_deref()` empty-and-shape gate binding, the
443 /// caixa-helm `build_chart_yaml` `caixa.repositorio.clone()`
444 /// `Chart.yaml` `home:` field fold, the caixa-helm `build_readme`
445 /// `caixa.repositorio.clone().unwrap_or_else(|| caixa.nome.clone())`
446 /// `README.md` `## Source` fold, and the caixa-flux
447 /// `ClusterBundleOpts::for_caixa`
448 /// `caixa.repositorio.clone().unwrap_or_else(|| format!(...))`
449 /// `GitRepository.spec.url` fold — four open-coded field-accesses
450 /// that expressed no compile-time link back to the typed slot. A
451 /// future extension of the `:repositorio` axis to a richer author
452 /// surface — a per-`:repositorio` structured
453 /// [`crate::render::GitRepoUrl`]-shaped scheme+host+path parse
454 /// (the future tightening [`Self::validate_repositorio`]'s
455 /// docstring anticipates alongside the peer per-`:deps :fonte
456 /// :repo` axis), a per-cluster repo-mirror overlay the M4 CR
457 /// materializer resolves per-CR (the "cluster policy rewrites
458 /// `github:pleme-io/...` to `git.internal/mirror/pleme-io/...`"
459 /// arm the private-registry story acknowledges), a promotion of
460 /// the plain `Option<String>` byte-string to a richer
461 /// `RepoUrl` enum discriminated on scheme — would have had to be
462 /// threaded through all four open-coded copies in lockstep or the
463 /// validate gate and the three emit paths would silently disagree
464 /// on which URL a given [`Caixa`] resolves to (an author's
465 /// `:repositorio "github:pleme-io/checkout"` would satisfy validate
466 /// while one of the emit paths silently rendered a stale URL, or
467 /// vice versa). Lifting the resolution to a typed method on the
468 /// substrate primitive means every downstream consumer of the
469 /// caixa's per-`Caixa` repo-URL surface reaches for exactly one
470 /// typed dispatch — the resolver's accept-set migrates as a unit on
471 /// any future axis addition.
472 ///
473 /// Second outer top-level [`Caixa`] `Option<&str>`-return scalar
474 /// accessor — sibling of [`Self::licenca`] (6d5bc28), the accessor
475 /// that opened the "outer [`Caixa`] `Option<&str>` scalar"
476 /// projection pattern this lift folds on. Same "one typed dispatch
477 /// on the substrate primitive, thin projections at each consumer"
478 /// discipline the peer per-`:placement`
479 /// [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
480 /// [`crate::aplicacao::Placement::affinity`] (74ec2d3) /
481 /// per-`:contratos` [`crate::aplicacao::WitContract::endpoint`]
482 /// (7020470) / [`crate::aplicacao::WitContract::subject`] (90de675)
483 /// / [`crate::aplicacao::WitContract::slot`] (ed22b66)
484 /// `Option<&str>`-return accessors carry on the sibling M2 / M3
485 /// typed-slot atom axes, extended here to the second outer top-level
486 /// `Caixa` universal-axis surface. Named `repositorio()` to match
487 /// the storage field's name; the accessor's identity maps onto the
488 /// canonical CAIXA-SDLC §I vocabulary the slot's docstring already
489 /// carries.
490 #[must_use]
491 pub fn repositorio(&self) -> Option<&str> {
492 self.repositorio.as_deref()
493 }
494
495 /// Substrate-canonical per-`Caixa` `:descricao` free-form-prose
496 /// chart-description scalar accessor every consumer of the top-level
497 /// manifest's Chart.yaml `description:` axis keys off — returns the
498 /// author-declared `:descricao` byte-string verbatim as an
499 /// `Option<&str>`, borrowed from the typed slot's own
500 /// `Option<String>` storage. `None` when the slot is absent (the
501 /// canonical "omit to defer to the per-renderer `caixa.nome`-derived
502 /// fallback" shape — [`caixa-helm`]'s `build_chart_yaml` folds the
503 /// omitted slot through a `format!("Generated chart for caixa Servico
504 /// {}", caixa.nome)` fallback, [`caixa-helm`]'s `build_readme` folds
505 /// it through a `format!("caixa Servico {}", caixa.nome)` fallback,
506 /// and [`caixa-feira`]'s `render_flake` folds it through a
507 /// `format!("caixa {}", c.nome)` `flake.nix` `description = ""`
508 /// fallback — each derived from `caixa.nome` on the null-carrier arm).
509 ///
510 /// The `:descricao` slot carries the universal-axis free-form-prose
511 /// chart-description identifier every kind of caixa emits under
512 /// (CAIXA-SDLC §I — the author-facing surface every `defcaixa` form
513 /// supplies) — the typed slot's `Option<String>` accept-set
514 /// (empty-string rejected through [`ManifestError::DescricaoEmpty`],
515 /// chart-description-shape-invalid rejected through
516 /// [`ManifestError::DescricaoInvalid`] past the shared
517 /// [`crate::render::is_chart_description_shape`] predicate the peer
518 /// per-`Caixa` `:descricao` axis also routes through) maps onto four
519 /// load-bearing downstream consumers:
520 ///
521 /// - [`Self::validate_descricao`]'s empty-arm + shape-predicate
522 /// gate binding — the universal-axis identity gate wired at
523 /// caixa-build time.
524 /// - [`caixa-helm`]'s `build_chart_yaml` `ChartYaml.description`
525 /// `Chart.yaml` field fold — the rendered `lareira-<nome>` Helm
526 /// chart's `Chart.yaml` `description:` field, which
527 /// `apiVersion: v2` charts require non-empty (`helm lint` fires
528 /// `WARNING [chart.metadata.description]: description is required`
529 /// when absent) and which every registry that ingests the chart
530 /// (ArtifactHub, chartmuseum, `helm search repo`) surfaces as the
531 /// chart's canonical one-line prose descriptor.
532 /// - [`caixa-helm`]'s `build_readme` chart-`README.md` header fold
533 /// — the rendered `lareira-<nome>` chart's `README.md` prose
534 /// header directly beneath the `# <chart-name>` title, which
535 /// every author who inspects the rendered chart bundle lands at.
536 /// - [`caixa-feira`]'s `render_flake` `flake.nix` `description = ""`
537 /// top-level fold — the emitted `flake.nix`'s `description`
538 /// field, which every Nix consumer (`nix flake show`,
539 /// `nix flake metadata`, downstream flake-registry ingestors)
540 /// surfaces as the flake's canonical descriptor.
541 ///
542 /// Prior to this lift the `.descricao` field was accessed inline at
543 /// four production sites — [`Self::validate_descricao`]'s
544 /// `self.descricao.as_deref()` empty-and-shape gate binding, the
545 /// caixa-helm `build_chart_yaml`
546 /// `caixa.descricao.clone().unwrap_or_else(|| format!(...))`
547 /// `Chart.yaml` `description:` fold, the caixa-helm `build_readme`
548 /// `caixa.descricao.clone().unwrap_or_else(|| format!(...))`
549 /// `README.md` header fold, and the caixa-feira `render_flake`
550 /// `c.descricao.clone().unwrap_or_else(|| format!(...))` `flake.nix`
551 /// `description = ""` fold — four open-coded field-accesses that
552 /// expressed no compile-time link back to the typed slot. A future
553 /// extension of the `:descricao` axis to a richer author surface —
554 /// a per-`:descricao` locale-tagged multi-language descriptor map
555 /// (the "one caixa, N language-tagged prose descriptions" arm
556 /// author-tooling internationalization anticipates), a
557 /// per-registry-target length-and-shape overlay the M4 CR
558 /// materializer resolves per-CR (the "ArtifactHub caps description
559 /// at 512 bytes but the internal registry caps at 256" arm), a
560 /// promotion of the plain `Option<String>` byte-string to a richer
561 /// `ChartDescription` newtype guaranteeing the
562 /// `is_chart_description_shape` predicate at the type level — would
563 /// have had to be threaded through all four open-coded copies in
564 /// lockstep or the validate gate and the three emit paths would
565 /// silently disagree on which prose string a given [`Caixa`]
566 /// resolves to (an author's
567 /// `:descricao "Checkout flow orchestration."` would satisfy
568 /// validate while one of the emit paths silently rendered a stale
569 /// `caixa.nome`-derived fallback, or vice versa). Lifting the
570 /// resolution to a typed method on the substrate primitive means
571 /// every downstream consumer of the caixa's per-`Caixa`
572 /// chart-description surface reaches for exactly one typed dispatch
573 /// — the resolver's accept-set migrates as a unit on any future
574 /// axis addition.
575 ///
576 /// Third outer top-level [`Caixa`] `Option<&str>`-return scalar
577 /// accessor — sibling of [`Self::licenca`] (6d5bc28) and
578 /// [`Self::repositorio`] (cc7332d), the accessors that opened the
579 /// "outer [`Caixa`] `Option<&str>` scalar" projection pattern this
580 /// lift folds on. Same "one typed dispatch on the substrate
581 /// primitive, thin projections at each consumer" discipline the
582 /// peer per-`:placement`
583 /// [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
584 /// [`crate::aplicacao::Placement::affinity`] (74ec2d3) /
585 /// per-`:contratos` [`crate::aplicacao::WitContract::endpoint`]
586 /// (7020470) / [`crate::aplicacao::WitContract::subject`] (90de675)
587 /// / [`crate::aplicacao::WitContract::slot`] (ed22b66)
588 /// `Option<&str>`-return accessors carry on the sibling M2 / M3
589 /// typed-slot atom axes, extended here to the third outer top-level
590 /// `Caixa` universal-axis surface. Named `descricao()` to match the
591 /// storage field's name; the accessor's identity maps onto the
592 /// canonical CAIXA-SDLC §I vocabulary the slot's docstring already
593 /// carries. The one remaining universal `Option<String>` slot
594 /// (`:edicao`) folds on this pattern next.
595 #[must_use]
596 pub fn descricao(&self) -> Option<&str> {
597 self.descricao.as_deref()
598 }
599
600 /// Substrate-canonical per-`Caixa` `:edicao` language-edition scalar
601 /// accessor every consumer of the top-level manifest's tatara-lisp
602 /// edition-selector axis keys off — returns the author-declared
603 /// `:edicao` byte-string verbatim as an `Option<&str>`, borrowed from
604 /// the typed slot's own `Option<String>` storage. `None` when the
605 /// slot is absent (the canonical "omit the slot to defer to the
606 /// substrate's default edition" shape every existing
607 /// [`caixa-resolver`] integration test fixture carries via
608 /// `edicao: None` — see `caixa-resolver/tests/git_integration.rs`;
609 /// the peer [`Self::validate_edicao`] gate is a no-op on the omitted
610 /// arm by construction, so an author-omitted `:edicao` round-trips
611 /// to a build without triggering the year-shape predicate).
612 ///
613 /// The `:edicao` slot carries the universal-axis 4-digit-ASCII-
614 /// decimal-year language-edition identifier every kind of caixa
615 /// emits under (CAIXA-SDLC §I — the author-facing surface every
616 /// `defcaixa` form supplies) — the typed slot's `Option<String>`
617 /// accept-set (empty-string rejected through
618 /// [`ManifestError::EdicaoEmpty`], year-shape-invalid rejected
619 /// through [`ManifestError::EdicaoInvalid`] past the 4-digit-ASCII-
620 /// decimal-year predicate [`Self::validate_edicao`] enforces) maps
621 /// onto one load-bearing downstream consumer today
622 /// ([`Self::validate_edicao`]'s empty-arm + year-shape-predicate
623 /// gate binding at caixa-core/src/manifest.rs:1959) plus every
624 /// future edition-aware substrate consumer the CAIXA-SDLC §I
625 /// roadmap anticipates (the tatara-lisp compiler's macro-surface
626 /// selector every edition-aware build step keys off, the future
627 /// per-edition compatibility-flag overlay the M4 CR materializer
628 /// resolves per-CR, the peer [`Caixa::template`] canonical
629 /// `:edicao "2026"` scaffold every `feira init` emits verbatim,
630 /// and the renderer-side fixtures at `caixa-helm/src/lib.rs:978` /
631 /// `caixa-flux/src/lib.rs:2319` / `caixa-mesh/src/lib.rs:3208` that
632 /// carry `edicao: Some("2026".into())` by construction).
633 ///
634 /// Prior to this lift the `.edicao` field was accessed inline at
635 /// one production site — [`Self::validate_edicao`]'s
636 /// `self.edicao.as_deref()` empty-and-shape gate binding — one
637 /// open-coded field-access that expressed no compile-time link
638 /// back to the typed slot. A future extension of the `:edicao`
639 /// axis to a richer author surface — a per-`:edicao` known-
640 /// edition allowlist (the future tightening
641 /// [`Self::validate_edicao`]'s docstring acknowledges past the
642 /// structural year-shape floor, rejecting year-shaped values that
643 /// don't name a tatara-lisp edition the substrate actually
644 /// understands — `"1999"` is year-shaped but no `1999` edition
645 /// exists), a per-edition compatibility-flag overlay the M4 CR
646 /// materializer resolves per-CR (the "edition `"2026"` enables
647 /// macro-surface features the sibling `"2018"` gates behind a
648 /// feature flag" arm the edition-selector story anticipates), a
649 /// promotion of the plain `Option<String>` byte-string to a
650 /// richer `CaixaEdition` enum discriminated on year once a sibling
651 /// edition to `"2026"` lands — would have had to be threaded
652 /// through the open-coded copy in lockstep with every future
653 /// edition-aware consumer, or the validate gate and the future
654 /// edition-aware consumer path would silently disagree on which
655 /// edition a given [`Caixa`] resolves to (an author's
656 /// `:edicao "2026"` would satisfy validate while a future
657 /// edition-aware consumer silently defaulted to a stale edition,
658 /// or vice versa). Lifting the resolution to a typed method on
659 /// the substrate primitive means every downstream consumer of the
660 /// caixa's per-`Caixa` edition surface reaches for exactly one
661 /// typed dispatch — the resolver's accept-set migrates as a unit
662 /// on any future axis addition.
663 ///
664 /// Fourth and final outer top-level [`Caixa`] `Option<&str>`-return
665 /// scalar accessor — sibling of [`Self::licenca`] (6d5bc28),
666 /// [`Self::repositorio`] (cc7332d), and [`Self::descricao`]
667 /// (3f16e2f), the accessors that opened the "outer [`Caixa`]
668 /// `Option<&str>` scalar" projection pattern this lift folds on.
669 /// Same "one typed dispatch on the substrate primitive, thin
670 /// projections at each consumer" discipline the peer per-`:placement`
671 /// [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
672 /// [`crate::aplicacao::Placement::affinity`] (74ec2d3) /
673 /// per-`:contratos` [`crate::aplicacao::WitContract::endpoint`]
674 /// (7020470) / [`crate::aplicacao::WitContract::subject`] (90de675)
675 /// / [`crate::aplicacao::WitContract::slot`] (ed22b66)
676 /// `Option<&str>`-return accessors carry on the sibling M2 / M3
677 /// typed-slot atom axes, extended here to close the outer top-level
678 /// `Caixa` universal-axis surface's last unlifted `Option<String>`
679 /// slot. Named `edicao()` to match the storage field's name; the
680 /// accessor's identity maps onto the canonical CAIXA-SDLC §I
681 /// vocabulary the slot's docstring already carries.
682 #[must_use]
683 pub fn edicao(&self) -> Option<&str> {
684 self.edicao.as_deref()
685 }
686
687 /// Substrate-canonical per-`Caixa` `:nome` universal-axis DNS-1123-
688 /// label caixa-identity scalar accessor every consumer of the top-
689 /// level manifest's identity axis keys off — returns the author-
690 /// declared `:nome` byte-string verbatim as an `&str`, borrowed from
691 /// the typed slot's own `String` storage. Non-optional (`:nome` is
692 /// a required-axis scalar every `defcaixa` form must supply; the
693 /// [`Self::from_lisp`] derive rejects an omitted / non-string
694 /// `:nome` at parse time, so a `Caixa` past parse definitionally
695 /// carries a non-`None` `:nome`).
696 ///
697 /// The `:nome` slot carries the universal-axis DNS-1123-label
698 /// caixa-identity every kind of caixa emits under (CAIXA-SDLC §I —
699 /// the primary identity axis every `defcaixa` form supplies
700 /// alongside `:versao` / `:kind`; the substrate-wide identity every
701 /// other typed surface that names a caixa reaches through — `:deps`
702 /// entries, `:membros` entries, `:children` entries, the
703 /// `lareira-<nome>` Helm chart name every per-Servico renderer
704 /// derives, the `pleme-program-<nome>` label every per-Aplicacao
705 /// renderer emits) — the typed slot's `String` accept-set (empty
706 /// rejected through [`ManifestError::NomeEmpty`], DNS-1123-shape-
707 /// invalid rejected through [`ManifestError::NomeInvalid`] past
708 /// the shared [`crate::render::require_valid_dns_1123_label`] gate
709 /// the peer name axes each land on, joint-length-with-`lareira-`-
710 /// prefix rejected through
711 /// [`ManifestError::NomeChartNameBudgetExceeded`] past
712 /// [`crate::render::is_lareira_chart_name_shape`]) maps onto every
713 /// load-bearing downstream consumer the substrate carries — the
714 /// two universal-axis validate gates at caixa-build time
715 /// ([`Self::validate_nome`] + [`Self::validate_nome_chart_name_budget`]),
716 /// [`crate::lareira_chart_name`]'s `lareira-<nome>` Helm chart-name
717 /// derivation every per-Servico renderer keys off, the caixa-helm
718 /// `Chart.yaml`'s `name:` axis, caixa-flux's `programs.yaml` entry
719 /// `name:` axis, caixa-mesh's Cilium `CiliumNetworkPolicy` /
720 /// `HTTPRoute` per-Aplicacao name axes at
721 /// caixa-mesh/src/lib.rs:{2650, 2797, 2919, 2925},
722 /// [`crate::pleme_program_selector`] /
723 /// [`crate::pleme_program_in_aplicacao_selector`] label-selector
724 /// derivations, and every future substrate renderer that emits an
725 /// artifact keyed by the caixa's identity.
726 ///
727 /// Prior to this lift the `.nome` field was accessed inline at a
728 /// dozen production sites across `caixa-core` (the two universal-
729 /// axis validate gates + [`Dep::validate`]-adjacent duplicate
730 /// tracking), `caixa-helm` (the `lareira_chart_name` fold, the
731 /// `ChartYaml.name` / `ChartYaml.description` / `Chart.yaml`
732 /// `keywords` fallback), `caixa-flux` (the `programs.yaml`
733 /// entry `name:` fold, the `flux_kustomization_source_subtree`
734 /// per-cluster subpath derivation), and `caixa-mesh` (the
735 /// `pleme_program_in_aplicacao_selector` label-selector fold, the
736 /// `cilium_network_policy_name` / `gateway_api_http_route_name`
737 /// per-CR name derivations, the `LABEL_APLICACAO` labels-map
738 /// insert) — a dozen open-coded field-accesses that expressed no
739 /// compile-time link back to the typed slot. A future extension of
740 /// the `:nome` axis to a richer author surface — a per-`:nome`
741 /// structured `CaixaIdentity` newtype that carries the joint-
742 /// length-with-prefix invariant [`Self::validate_nome_chart_name_budget`]
743 /// enforces at the type level (rather than as a validate-time
744 /// gate), a per-registry `:nome` namespacing overlay the M4 CR
745 /// materializer resolves per-CR (the "`pleme-io/checkout` vs
746 /// `partner-org/checkout` collision" arm the multi-tenant-registry
747 /// story acknowledges), a promotion of the plain `String` byte-
748 /// string to a richer `CaixaNome` newtype discriminated on
749 /// namespace prefix — would have had to be threaded through every
750 /// open-coded copy in lockstep or the two validate gates and the
751 /// dozen emit paths would silently disagree on which identity a
752 /// given [`Caixa`] resolves to (an author's `:nome "checkout"`
753 /// would satisfy validate while one of the emit paths silently
754 /// rendered a drifted other identity, or vice versa). Lifting the
755 /// resolution to a typed method on the substrate primitive means
756 /// every downstream consumer of the caixa's per-`Caixa` identity
757 /// surface reaches for exactly one typed dispatch — the resolver's
758 /// accept-set migrates as a unit on any future axis addition.
759 ///
760 /// First outer top-level [`Caixa`] `&str`-return required-scalar
761 /// accessor — opens the "outer [`Caixa`] `&str` required-scalar"
762 /// projection pattern the sibling per-`Caixa` `:versao` future lift
763 /// folds on. Sibling in shape to the peer per-`:membros`
764 /// [`crate::aplicacao::Membro::nome`] (4a32abf) / per-`:contratos`
765 /// [`crate::aplicacao::WitContract::source`] /
766 /// [`crate::aplicacao::WitContract::destination`] (7f0fd43),
767 /// [`crate::aplicacao::WitContract::world_ref`] (0804823),
768 /// [`crate::aplicacao::Membro::versao_requirement`] (a40b0e3),
769 /// [`crate::aplicacao::Entrada::destination`] (6db982c),
770 /// [`crate::aplicacao::CircuitBreaker::max_failures`] (3a74062),
771 /// per-sub-struct required-axis accessors carry on the sibling M3
772 /// mesh-slot-atom scalar-value axes, extended here to open the
773 /// outer top-level [`Caixa`] `&str`-return required-scalar surface.
774 /// Named `nome()` to match the storage field's name; the accessor's
775 /// identity maps onto the canonical CAIXA-SDLC §I vocabulary the
776 /// slot's docstring already carries.
777 #[must_use]
778 pub fn nome(&self) -> &str {
779 &self.nome
780 }
781
782 /// Substrate-canonical per-`Caixa` `:versao` universal-axis SemVer-2
783 /// pinned-version scalar accessor every consumer of the top-level
784 /// manifest's version axis keys off — returns the author-declared
785 /// `:versao` byte-string verbatim as an `&str`, borrowed from the
786 /// typed slot's own `String` storage. Non-optional (`:versao` is a
787 /// required-axis scalar every `defcaixa` form must supply alongside
788 /// `:nome` / `:kind`; the [`Self::from_lisp`] derive rejects an
789 /// omitted / non-string `:versao` at parse time, so a `Caixa` past
790 /// parse definitionally carries a non-`None` `:versao`).
791 ///
792 /// The `:versao` slot carries the universal-axis SemVer-2
793 /// concrete-version body every kind of caixa emits under
794 /// (CAIXA-SDLC §I — the required-scalar every `defcaixa` form
795 /// supplies alongside `:nome` / `:kind`; the substrate-wide
796 /// pinned-version every downstream artifact-emitting consumer
797 /// composes under — the `lareira-<nome>` Helm chart's `Chart.yaml`
798 /// `version:` + `appVersion:` axes, the `feira publish` Zig-style
799 /// `v<versao>` git tag the [`crate::DEFAULT_PUBLISH_TAG_PREFIX`]
800 /// prefix composes on top of, the programs.yaml entry's `versao:`
801 /// value the `lareira-fleet-programs` aggregator carries onto each
802 /// rendered `ComputeUnit`, the OCI image's `:v<versao>` / `:latest`
803 /// tags every substrate-side `skopeo push` writes, the lacre
804 /// closure's pinned `concrete_versao`, and the `:upgrade-from :from`
805 /// prior-version references peers in the exact same SemVer-2 shape).
806 /// The typed slot's `String` accept-set (empty rejected through
807 /// [`ManifestError::VersaoEmpty`], SemVer-2-shape-invalid rejected
808 /// through [`ManifestError::VersaoInvalid`] past
809 /// [`semver::Version::parse`]) maps onto every load-bearing
810 /// downstream consumer the substrate carries — the [`Self::validate_versao`]
811 /// universal-axis validate gate at caixa-build time, the
812 /// [`crate::CaixaVersion::parse`] typed-wrapper resolver,
813 /// [`caixa-helm`]'s `Chart.yaml` `version:` / `appVersion:` fold,
814 /// [`caixa-flux`]'s `programs.yaml` entry `versao:` fold + the
815 /// `cluster_bundle` `GitRepository` `ref: { tag: v<versao> }`
816 /// derivation, [`caixa-mesh`]'s per-Aplicacao `programs.yaml` fan-
817 /// out entry `versao:` fold, [`caixa-feira`]'s `feira publish` git-
818 /// tag derivation (`format!("{prefix}{versao}")`), and every future
819 /// substrate renderer that emits an artifact keyed by the caixa's
820 /// pinned version.
821 ///
822 /// Prior to this lift the `.versao` field was accessed inline at a
823 /// dozen production sites across `caixa-core` (the universal-axis
824 /// [`Self::validate_versao`] gate + [`Dep::validate`]-adjacent
825 /// version-shape gates), `caixa-helm` (the `ChartYaml.version` /
826 /// `ChartYaml.app_version` folds), `caixa-flux` (the `programs.yaml`
827 /// entry `versao:` fold, the `cluster_bundle` `GitRepository` `ref:
828 /// { tag: v<versao> }` derivation), `caixa-mesh` (the per-Aplicacao
829 /// `programs.yaml` fan-out entry `versao:` fold), and `caixa-feira`
830 /// (the `feira publish` git-tag derivation + the `feira app graph` /
831 /// `feira app deploy` diagnostic renderers) — a dozen open-coded
832 /// field-accesses that expressed no compile-time link back to the
833 /// typed slot. A future extension of the `:versao` axis to a richer
834 /// author surface — a per-`:versao` structured `CaixaVersion` at the
835 /// storage layer (the substrate already carries a `CaixaVersion`
836 /// newtype at [`crate::version::CaixaVersion`], deferred until the
837 /// serde-transparent-newtype-through-DeriveTataraDomain path lands),
838 /// a per-registry `:versao` immutability overlay the M4 CR
839 /// materializer enforces per-CR, a promotion of the plain `String`
840 /// byte-string to a richer `PinnedVersao` newtype discriminated on
841 /// SemVer-2 pre-release / build-metadata presence — would have had
842 /// to be threaded through every open-coded copy in lockstep or the
843 /// validate gate and the dozen emit paths would silently disagree
844 /// on which version a given [`Caixa`] resolves to (an author's
845 /// `:versao "0.1.0"` would satisfy validate while one of the emit
846 /// paths silently rendered a drifted other version, or vice versa).
847 /// Lifting the resolution to a typed method on the substrate
848 /// primitive means every downstream consumer of the caixa's
849 /// per-`Caixa` pinned-version surface reaches for exactly one typed
850 /// dispatch — the resolver's accept-set migrates as a unit on any
851 /// future axis addition.
852 ///
853 /// Second outer top-level [`Caixa`] `&str`-return required-scalar
854 /// accessor — folds on the "outer [`Caixa`] `&str` required-scalar"
855 /// projection pattern the sibling per-`Caixa` [`Self::nome`]
856 /// (e6b7d97) opened. Sibling in shape to the peer per-`:membros`
857 /// [`crate::aplicacao::Membro::versao_requirement`] (4127bb6) /
858 /// per-`:children` [`crate::supervisor::ChildSpec::versao_requirement`]
859 /// (2c053c8) / per-`:upgrade-from` [`crate::UpgradeFromEntry::prior_versao`]
860 /// (75d27a8) per-sub-struct `:versao`-shaped `&str`-return accessors
861 /// on the sibling per-typed-slot version-carrier axes, extended here
862 /// to close the second outer top-level [`Caixa`] required-`&str`-
863 /// carrying axis so the two universal-axis identity-carrying
864 /// scalars every `defcaixa` form supplies (`:nome` + `:versao`)
865 /// share the same "one typed dispatch per axis" discipline. Named
866 /// `versao()` to match the storage field's name; the accessor's
867 /// identity maps onto the canonical CAIXA-SDLC §I vocabulary the
868 /// slot's docstring already carries.
869 #[must_use]
870 pub fn versao(&self) -> &str {
871 &self.versao
872 }
873
874 /// Substrate-canonical per-`Caixa` `:kind` universal-axis
875 /// closed-set-enum discriminant accessor every consumer of the top-
876 /// level manifest's kind axis keys off — returns the author-declared
877 /// `:kind` variant verbatim as a [`CaixaKind`], `Copy`-projected
878 /// from the typed slot's own [`CaixaKind`] storage. Non-optional
879 /// (`:kind` is a required-axis discriminant every `defcaixa` form
880 /// must supply alongside `:nome` / `:versao`; the [`Self::from_lisp`]
881 /// derive rejects an omitted / non-symbol `:kind` at parse time, so
882 /// a `Caixa` past parse definitionally carries a valid [`CaixaKind`]
883 /// variant).
884 ///
885 /// The `:kind` slot carries the universal-axis closed-set typed-
886 /// discriminant every substrate-side dispatch keys off (CAIXA-SDLC
887 /// §I — the primary shape gate every renderer / verifier /
888 /// operator branches on; the five variants `Biblioteca` /
889 /// `Binario` / `Servico` / `Supervisor` / `Aplicacao` partition
890 /// the caixa surface into disjoint runtime contracts) — the typed
891 /// slot's [`CaixaKind`] accept-set (parse-time-rejected non-symbol
892 /// values through the derive-macro's symbol-arm gate, exhaustively
893 /// matched at every downstream dispatch site) maps onto every
894 /// load-bearing downstream consumer the substrate carries:
895 ///
896 /// - [`crate::render::require_kind`]'s per-renderer entry-gate
897 /// predicate — the canonical two-line
898 /// `require_kind(caixa, Servico)?` prelude every per-Servico
899 /// renderer (`caixa-helm`, `caixa-flux`, the future `caixa-otel`
900 /// / per-Servico OCI packager / M4 `wasm.pleme.io/v1alpha1/
901 /// ComputeUnit` CR materializer) runs at its entry-point,
902 /// alongside the [`crate::render::KindMismatch`] error carrier's
903 /// `actual:` field the diagnostic surfaces to name the offending
904 /// caixa's variant.
905 /// - [`Self::aplicacao_view`]'s + [`Self::supervisor_view`]'s
906 /// per-view kind-gate binding — the two `Option<TypedSpec>`
907 /// `_view` composers that fold the flat mesh-slot / supervisor-
908 /// slot columns into their typed sub-spec only when the kind
909 /// matches (returns `None` otherwise); the future per-Servico
910 /// M2-view composer (`servico_view`) will follow the same shape.
911 /// - [`Self::declared_foreign_code_slots`]'s per-slot kind-
912 /// coherence gate — the `!self.kind.requires_exe()` /
913 /// `!self.kind.requires_servicos()` predicates that fence
914 /// each code-surface slot from the wrong owning kind.
915 /// - [`crate::LayoutInvariants::verify`]'s kind ↔ code-surface
916 /// coherence gates — the six `caixa.kind == CaixaKind::X` /
917 /// `caixa.kind != CaixaKind::X` predicates and the four kind-
918 /// coherence error carriers (`SupervisorOwnsCode` /
919 /// `AplicacaoOwnsCode` / `MeshSlotsOnNonAplicacao` /
920 /// `SupervisorSlotsOnNonSupervisor` / `ServicoSlotsOnNonServico`
921 /// / `ForeignCodeSlot`) which each name the offending caixa's
922 /// variant in their `kind:` field.
923 ///
924 /// Prior to this lift the `.kind` field was accessed inline at
925 /// twenty-plus production sites across `caixa-core` (the
926 /// [`crate::render::require_kind`] entry-gate predicate + the
927 /// [`crate::render::KindMismatch`] `actual:` field, the two `_view`
928 /// composers, the `declared_foreign_code_slots` per-slot kind-
929 /// coherence gate, and the six [`crate::LayoutInvariants::verify`]
930 /// kind ↔ code-surface predicates + four error carriers) — a score
931 /// of open-coded field-accesses that expressed no compile-time link
932 /// back to the typed slot. A future extension of the `:kind` axis
933 /// to a richer author surface — a per-`:kind` sub-variant discriminant
934 /// (e.g. `Servico(ServicoRuntime)` splitting the current single
935 /// variant across the wasm-component / legacy-container / native-
936 /// binary runtime axes the M5 roadmap acknowledges), a per-cluster
937 /// kind-overlay the M4 CR materializer resolves per-CR (the
938 /// "cluster policy demotes `Aplicacao` to `Servico` on a single-
939 /// tenant cluster" arm), a promotion of the plain [`CaixaKind`]
940 /// enum to a richer `KindWithRuntime` discriminated on the
941 /// component-model world axis — would have had to be threaded
942 /// through every open-coded copy in lockstep or the entry gate,
943 /// the view composers, and the layout invariants would silently
944 /// disagree on which kind a given [`Caixa`] resolves to. Lifting
945 /// the resolution to a typed method on the substrate primitive
946 /// means every downstream consumer of the caixa's per-`Caixa`
947 /// kind surface reaches for exactly one typed dispatch — the
948 /// resolver's accept-set migrates as a unit on any future axis
949 /// addition.
950 ///
951 /// First outer top-level [`Caixa`] `Copy`-return required-enum-
952 /// discriminant accessor — opens the "outer [`Caixa`] `Copy`-return
953 /// required-discriminant" projection pattern. Sibling in shape to
954 /// the peer per-`:supervisor` [`crate::supervisor::SupervisorSpec::estrategia`]
955 /// (eafb619), per-`:placement` [`crate::aplicacao::Placement::estrategia`]
956 /// (921fe1b), and per-`:children` [`crate::supervisor::ChildSpec::restart`]
957 /// (dfb4a81) `Copy`-return closed-set-enum discriminant accessors
958 /// on the sibling nested-spec typed-slot discriminator axes,
959 /// extended here to the outer top-level [`Caixa`] universal-axis
960 /// surface. Named `kind()` to match the storage field's name;
961 /// the accessor's identity maps onto the canonical CAIXA-SDLC §I
962 /// vocabulary the slot's docstring already carries.
963 #[must_use]
964 pub fn kind(&self) -> CaixaKind {
965 self.kind
966 }
967
968 /// Substrate-canonical per-`Caixa` `:autores` universal-axis
969 /// maintainer-name-list slice-accessor every consumer of the top-
970 /// level manifest's maintainer axis keys off — returns the author-
971 /// declared `:autores` list verbatim as a `&[String]` slice-view over
972 /// the same backing buffer the raw `self.autores.as_slice()` field
973 /// access borrows from. Empty-list-carrying (`:autores` is a default-
974 /// empty axis every `defcaixa` form supplies with an empty `()` when
975 /// unset; the [`Self::from_lisp`] derive folds an omitted `:autores`
976 /// through `#[serde(default)]` to `Vec::new()`, so a `Caixa` past
977 /// parse definitionally carries a `Vec<String>` slot — possibly
978 /// empty — and the returned `&[String]` degenerates to an empty
979 /// slice on that arm without any silent `None` collapse).
980 ///
981 /// The `:autores` slot carries the universal-axis maintainer-name
982 /// list every kind of caixa emits under (CAIXA-SDLC §I — the author-
983 /// facing surface every `defcaixa` form supplies alongside `:nome` /
984 /// `:versao` / `:kind`; the substrate-wide contact-carrying axis
985 /// every downstream registry-facing artifact emits under) — the
986 /// typed slot's `Vec<String>` accept-set (empty-per-entry rejected
987 /// through [`ManifestError::AutorEmpty`], non-chart-maintainer-shape
988 /// rejected through [`ManifestError::AutorInvalid`], cross-entry
989 /// duplicate rejected through [`ManifestError::AutorDuplicate`]) maps
990 /// onto every load-bearing downstream consumer the substrate carries
991 /// — the [`Self::validate_autores`] universal-axis empty-per-entry +
992 /// shape + duplicate gate at caixa-core/src/manifest.rs, the
993 /// caixa-helm `build_chart_yaml` `maintainers:` fold at
994 /// caixa-helm/src/lib.rs that walks each entry into a `Maintainer {
995 /// name, email: None }` record, every future per-`Caixa` registry-
996 /// facing renderer the CAIXA-SDLC §I roadmap acknowledges (the
997 /// future `artifacthub.io/maintainers` `Chart.yaml` annotation the
998 /// caixa-helm docstring alludes to at [`Self::validate_licenca`],
999 /// the future per-cluster author-notification overlay the M4 CR
1000 /// materializer resolves per-CR).
1001 ///
1002 /// Prior to this lift the `.autores` field was accessed inline at
1003 /// two production sites — [`Self::validate_autores`]'s `for autor
1004 /// in &self.autores` walk that gates every entry through
1005 /// [`ManifestError::AutorEmpty`] / `AutorInvalid` / `AutorDuplicate`,
1006 /// and the caixa-helm `build_chart_yaml` `caixa.autores.iter().map(|a|
1007 /// Maintainer { name: a.clone(), email: None }).collect()` fold that
1008 /// materializes every entry into a `Chart.yaml` `maintainers:` row —
1009 /// two open-coded field-accesses that expressed no compile-time link
1010 /// back to the typed slot. A future extension of the `:autores` axis
1011 /// to a richer author surface — a per-`:autores` structured
1012 /// `Maintainer { name, email, url }` at the storage layer once the
1013 /// substrate absorbs `artifacthub.io/maintainers`' name+email+url
1014 /// tuple, a per-registry `:autores` allowlist the M4 CR materializer
1015 /// enforces per-CR (the "cluster policy demands every author declare
1016 /// an on-file `mailto:` contact" arm), a promotion of the plain
1017 /// `Vec<String>` byte-string list to a richer
1018 /// `Vec<ChartMaintainer>` newtype discriminated on the RFC-5322
1019 /// `<name> [<email>]` grammar the `is_chart_maintainer_name_shape`
1020 /// predicate already resolves through — would have had to be
1021 /// threaded through both open-coded copies in lockstep or the
1022 /// validate gate and the caixa-helm emit path would silently
1023 /// disagree on which authors a given [`Caixa`] resolves to (an
1024 /// author's `:autores ("alice" "bob")` would satisfy validate while
1025 /// the caixa-helm emit path silently rendered a drifted other
1026 /// maintainer list, or vice versa). Lifting the resolution to a
1027 /// typed method on the substrate primitive means every downstream
1028 /// consumer of the caixa's per-`Caixa` maintainer surface reaches
1029 /// for exactly one typed dispatch — the resolver's accept-set
1030 /// migrates as a unit on any future axis addition.
1031 ///
1032 /// First outer top-level [`Caixa`] `&[T]`-return slice-accessor —
1033 /// opens the "outer [`Caixa`] `&[T]` slice" projection pattern the
1034 /// sibling per-`Caixa` `:etiquetas` / `:deps` / `:deps-dev` / `:exe`
1035 /// / `:bibliotecas` / `:servicos` / `:upgrade-from` / `:children`
1036 /// future lifts fold on. Sibling in shape to the peer per-`:supervisor`
1037 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce), per-`:placement`
1038 /// [`crate::aplicacao::Placement::clusters`] (a6e18d7), per-`:membros`
1039 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36), per-`:contratos`
1040 /// [`crate::aplicacao::AplicacaoSpec::contratos`] (0dcc926), and
1041 /// per-`:upgrade-from :instructions` [`crate::upgrade::UpgradeFromEntry::instructions`]
1042 /// (0137e5a) `&[T]`-return slice accessors on the sibling per-M2 /
1043 /// per-M3 typed-slot list axes, extended here to the outer top-level
1044 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1045 /// `&Vec<String>`) because every downstream consumer of the author
1046 /// list treats it as a read-only sequence — the slice-view is the
1047 /// narrowest borrow that supports every present + roadmapped consumer
1048 /// (`.iter()`, `.len()`, `.is_empty()`) without leaking the backing
1049 /// `Vec`'s grow/push/reserve surface no consumer of the typed view
1050 /// reaches for (the storage-side `Vec` remains reachable through the
1051 /// `pub autores` field for the mutation-carrying serde round-trip and
1052 /// per-test fixture-mutation paths). Named `autores()` to match the
1053 /// storage field's name; the accessor's identity maps onto the
1054 /// canonical CAIXA-SDLC §I vocabulary the slot's docstring already
1055 /// carries.
1056 #[must_use]
1057 pub fn autores(&self) -> &[String] {
1058 self.autores.as_slice()
1059 }
1060
1061 /// Substrate-canonical per-`Caixa` `:etiquetas` universal-axis
1062 /// registry-search-tag-list slice-accessor every consumer of the
1063 /// top-level manifest's topical-tag axis keys off — returns the
1064 /// author-declared `:etiquetas` list verbatim as a `&[String]`
1065 /// slice-view over the same backing buffer the raw
1066 /// `self.etiquetas.as_slice()` field access borrows from. Empty-
1067 /// list-carrying (`:etiquetas` is a default-empty axis every
1068 /// `defcaixa` form supplies with an empty `()` when unset; the
1069 /// [`Self::from_lisp`] derive folds an omitted `:etiquetas` through
1070 /// `#[serde(default)]` to `Vec::new()`, so a `Caixa` past parse
1071 /// definitionally carries a `Vec<String>` slot — possibly empty —
1072 /// and the returned `&[String]` degenerates to an empty slice on
1073 /// that arm without any silent `None` collapse).
1074 ///
1075 /// The `:etiquetas` slot carries the universal-axis topical-tag
1076 /// list every kind of caixa emits under (CAIXA-SDLC §I — the
1077 /// author-facing surface every `defcaixa` form supplies alongside
1078 /// `:nome` / `:versao` / `:kind`; the substrate-wide registry-
1079 /// search-facing axis every downstream registry-facing artifact
1080 /// emits under) — the typed slot's `Vec<String>` accept-set
1081 /// (empty-per-entry rejected through [`ManifestError::EtiquetaEmpty`],
1082 /// non-chart-keyword-shape rejected through
1083 /// [`ManifestError::EtiquetaInvalid`], cross-entry duplicate
1084 /// rejected through [`ManifestError::EtiquetaDuplicate`]) maps onto
1085 /// every load-bearing downstream consumer the substrate carries —
1086 /// the [`Self::validate_etiquetas`] universal-axis empty-per-entry
1087 /// + shape + duplicate gate at caixa-core/src/manifest.rs, the
1088 /// caixa-helm `build_chart_yaml` `keywords:` fold at
1089 /// caixa-helm/src/lib.rs that walks each entry into the rendered
1090 /// `Chart.yaml` `keywords:` array (chained with the
1091 /// [`crate::LAREIRA_CHART_KEYWORDS`] substrate-wide floor set and
1092 /// dedup'd through a `BTreeSet` at emit time), every future per-
1093 /// `Caixa` registry-facing renderer the CAIXA-SDLC §I roadmap
1094 /// acknowledges (the future `artifacthub.io/keywords` `Chart.yaml`
1095 /// annotation, the future per-cluster tag-notification overlay the
1096 /// M4 CR materializer resolves per-CR).
1097 ///
1098 /// Prior to this lift the `.etiquetas` field was accessed inline at
1099 /// two production sites — [`Self::validate_etiquetas`]'s `for
1100 /// etiqueta in &self.etiquetas` walk that gates every entry through
1101 /// [`ManifestError::EtiquetaEmpty`] / `EtiquetaInvalid` /
1102 /// `EtiquetaDuplicate`, and the caixa-helm `build_chart_yaml`
1103 /// `caixa.etiquetas.iter().cloned().chain(...)` fold that
1104 /// materializes every entry into a `Chart.yaml` `keywords:` row —
1105 /// two open-coded field-accesses that expressed no compile-time
1106 /// link back to the typed slot. A future extension of the
1107 /// `:etiquetas` axis to a richer tag surface — a per-`:etiquetas`
1108 /// structured `ChartKeyword { name, uri, category }` at the storage
1109 /// layer once the substrate absorbs `artifacthub.io/keywords`
1110 /// richer tag tuple, a per-registry `:etiquetas` allowlist the M4
1111 /// CR materializer enforces per-CR (the "cluster policy demands
1112 /// every tag come from a substrate-approved taxonomy" arm), a
1113 /// promotion of the plain `Vec<String>` byte-string list to a
1114 /// richer `Vec<ChartKeyword>` newtype discriminated on the DNS-
1115 /// 1123-label-shaped grammar the `is_chart_keyword_shape` predicate
1116 /// already resolves through — would have had to be threaded through
1117 /// both open-coded copies in lockstep or the validate gate and the
1118 /// caixa-helm emit path would silently disagree on which tags a
1119 /// given [`Caixa`] resolves to (an author's `:etiquetas ("demo"
1120 /// "aplicacao")` would satisfy validate while the caixa-helm emit
1121 /// path silently rendered a drifted other keyword list, or vice
1122 /// versa). Lifting the resolution to a typed method on the
1123 /// substrate primitive means every downstream consumer of the
1124 /// caixa's per-`Caixa` topical-tag surface reaches for exactly one
1125 /// typed dispatch — the resolver's accept-set migrates as a unit
1126 /// on any future axis addition.
1127 ///
1128 /// Second outer top-level [`Caixa`] `&[T]`-return slice-accessor —
1129 /// folds on the "outer [`Caixa`] `&[T]` slice" projection pattern
1130 /// [`Self::autores`] (b5d813f) opened, sibling in shape and
1131 /// idiom. The remaining unlifted outer-`Caixa` slice-carrying axes
1132 /// (`:deps` / `:deps-dev` / `:exe` / `:bibliotecas` / `:servicos`
1133 /// / `:upgrade-from` / `:children` / `:membros` / `:contratos`)
1134 /// fold onto the same pattern in future lifts. Sibling in shape to
1135 /// the peer per-`:supervisor`
1136 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
1137 /// per-`:placement` [`crate::aplicacao::Placement::clusters`]
1138 /// (a6e18d7), per-`:membros`
1139 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
1140 /// per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
1141 /// (0dcc926), and per-`:upgrade-from :instructions`
1142 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1143 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1144 /// typed-slot list axes, extended here to the outer top-level
1145 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1146 /// `&Vec<String>`) because every downstream consumer of the tag
1147 /// list treats it as a read-only sequence — the slice-view is the
1148 /// narrowest borrow that supports every present + roadmapped
1149 /// consumer (`.iter()`, `.len()`, `.is_empty()`) without leaking
1150 /// the backing `Vec`'s grow/push/reserve surface no consumer of
1151 /// the typed view reaches for (the storage-side `Vec` remains
1152 /// reachable through the `pub etiquetas` field for the mutation-
1153 /// carrying serde round-trip and per-test fixture-mutation paths).
1154 /// Named `etiquetas()` to match the storage field's name; the
1155 /// accessor's identity maps onto the canonical CAIXA-SDLC §I
1156 /// vocabulary the slot's docstring already carries.
1157 #[must_use]
1158 pub fn etiquetas(&self) -> &[String] {
1159 self.etiquetas.as_slice()
1160 }
1161
1162 /// Substrate-canonical per-`Caixa` `:bibliotecas` universal-axis
1163 /// library-source-path-list slice-accessor every consumer of the
1164 /// top-level manifest's Biblioteca-source axis keys off — returns
1165 /// the author-declared `:bibliotecas` list verbatim as a
1166 /// `&[String]` slice-view over the same backing buffer the raw
1167 /// `self.bibliotecas.as_slice()` field access borrows from. Empty-
1168 /// list-carrying (`:bibliotecas` is a default-empty axis every
1169 /// `defcaixa` form supplies with an empty `()` when unset; the
1170 /// [`Self::from_lisp`] derive folds an omitted `:bibliotecas`
1171 /// through `#[serde(default)]` to `Vec::new()`, so a `Caixa` past
1172 /// parse definitionally carries a `Vec<String>` slot — possibly
1173 /// empty — and the returned `&[String]` degenerates to an empty
1174 /// slice on that arm without any silent `None` collapse).
1175 ///
1176 /// The `:bibliotecas` slot carries the universal-axis lisp-library
1177 /// entry-path list every `:kind Biblioteca` caixa emits under
1178 /// (CAIXA-SDLC §I — the author-facing surface every `defcaixa`
1179 /// form supplies alongside `:nome` / `:versao` / `:kind`; the
1180 /// substrate-wide library-carrier axis every downstream
1181 /// authoring-facing consumer keys off) — the typed slot's
1182 /// `Vec<String>` accept-set (empty-per-entry rejected through
1183 /// [`ManifestError::CodePathEmpty { slot: ":bibliotecas" }`],
1184 /// non-sandboxed-relative-shape rejected through
1185 /// [`ManifestError::CodePathShape`], non-`.lisp`-extension rejected
1186 /// through [`ManifestError::CodePathNonLispExtension`], cross-entry
1187 /// duplicate rejected through [`ManifestError::CodePathDuplicate`])
1188 /// maps onto every load-bearing downstream consumer the substrate
1189 /// carries — the [`crate::LayoutInvariants`] Biblioteca-arm
1190 /// empty-check + per-entry file-exists loop at
1191 /// caixa-core/src/layout.rs that gates each entry through
1192 /// [`crate::LayoutError::MissingLib`] / `MissingEntry`, the
1193 /// [`Self::validate_code_paths`] per-slot shape gate at
1194 /// caixa-core/src/manifest.rs that walks each entry through the
1195 /// sandbox-relative / `.lisp`-extension / cross-entry duplicate
1196 /// gates, the `feira build` per-entry `tatara_lisp::read` parse
1197 /// walk at caixa-feira/src/cmd/build.rs that phase-1-checks each
1198 /// declared library file for lexical / structural errors before
1199 /// downstream `importar` resolution, every future per-`Caixa`
1200 /// library-facing renderer the CAIXA-SDLC §I roadmap acknowledges
1201 /// (the future `tatara-lispc` compilation entry the docstring at
1202 /// caixa-feira/src/cmd/build.rs alludes to, the future per-cluster
1203 /// bytecode-caching overlay the M4 CR materializer resolves per-CR,
1204 /// the future `caixa-lsp` per-library semantic-token stream the
1205 /// caixa-lsp docstring roadmaps).
1206 ///
1207 /// Prior to this lift the `.bibliotecas` field was accessed inline
1208 /// at three production sites — [`crate::LayoutInvariants`]'s
1209 /// `caixa.bibliotecas.is_empty()` `MissingLib`-arm gate + `for p
1210 /// in &caixa.bibliotecas` `MissingEntry` walk that gates each
1211 /// declared library path through the on-disk-existence check,
1212 /// the compound-code-path `has_code = !caixa.bibliotecas.is_empty()
1213 /// || !caixa.exe.is_empty() || !caixa.servicos.is_empty()` OR-fold
1214 /// on the [`crate::LayoutError::SupervisorOwnsCode`] /
1215 /// `AplicacaoOwnsCode` kind-coherence gate, and the `feira build`
1216 /// per-entry `for entry in &caixa.bibliotecas` + `caixa.bibliotecas.
1217 /// len()` phase-1 tatara-lispc-precursor parse walk — three open-
1218 /// coded field-accesses that expressed no compile-time link back
1219 /// to the typed slot. A future extension of the `:bibliotecas`
1220 /// axis to a richer library surface — a per-`:bibliotecas`
1221 /// structured `BibliotecaEntry { path, edition, exports }` at the
1222 /// storage layer once the substrate absorbs the per-library
1223 /// language-edition + explicit-exports tuple the tatara-lisp
1224 /// module-system roadmap acknowledges, a per-registry
1225 /// `:bibliotecas` allowlist the M4 CR materializer enforces
1226 /// per-CR (the "cluster policy demands every biblioteca declare
1227 /// its own :edicao" arm), a promotion of the plain `Vec<String>`
1228 /// byte-string list to a richer `Vec<LibraryPath>` newtype
1229 /// discriminated on the `lib/<nome>.lisp`-shape grammar the
1230 /// [`crate::render::is_sandboxed_relative_path`] +
1231 /// [`crate::render::is_lisp_extension`] predicates already resolve
1232 /// through — would have had to be threaded through all three
1233 /// open-coded copies in lockstep or the layout gate, the shape
1234 /// validator, and the `feira build` phase-1 parse walk would
1235 /// silently disagree on which library paths a given [`Caixa`]
1236 /// resolves to (an author's `:bibliotecas ("lib/foo.lisp"
1237 /// "lib/bar.lisp")` would satisfy layout while `feira build`
1238 /// silently parsed a drifted other list, or vice versa). Lifting
1239 /// the resolution to a typed method on the substrate primitive
1240 /// means every downstream consumer of the caixa's per-`Caixa`
1241 /// library-source surface reaches for exactly one typed dispatch
1242 /// — the resolver's accept-set migrates as a unit on any future
1243 /// axis addition.
1244 ///
1245 /// Third outer top-level [`Caixa`] `&[T]`-return slice-accessor —
1246 /// folds on the "outer [`Caixa`] `&[T]` slice" projection pattern
1247 /// [`Self::autores`] (b5d813f) opened and [`Self::etiquetas`]
1248 /// (78c7d3c) folded on, sibling in shape and idiom. The remaining
1249 /// unlifted outer-`Caixa` slice-carrying axes (`:deps` /
1250 /// `:deps-dev` / `:exe` / `:servicos` / `:upgrade-from` /
1251 /// `:children` / `:membros` / `:contratos`) fold onto the same
1252 /// pattern in future lifts. Sibling in shape to the peer
1253 /// per-`:supervisor`
1254 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
1255 /// per-`:placement` [`crate::aplicacao::Placement::clusters`]
1256 /// (a6e18d7), per-`:membros`
1257 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
1258 /// per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
1259 /// (0dcc926), and per-`:upgrade-from :instructions`
1260 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1261 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1262 /// typed-slot list axes, extended here to the outer top-level
1263 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1264 /// `&Vec<String>`) because every downstream consumer of the
1265 /// library-source list treats it as a read-only sequence — the
1266 /// slice-view is the narrowest borrow that supports every
1267 /// present + roadmapped consumer (`.iter()`, `.len()`,
1268 /// `.is_empty()`) without leaking the backing `Vec`'s
1269 /// grow/push/reserve surface no consumer of the typed view
1270 /// reaches for (the storage-side `Vec` remains reachable through
1271 /// the `pub bibliotecas` field for the mutation-carrying serde
1272 /// round-trip and per-test fixture-mutation paths). Named
1273 /// `bibliotecas()` to match the storage field's name; the
1274 /// accessor's identity maps onto the canonical CAIXA-SDLC §I
1275 /// vocabulary the slot's docstring already carries.
1276 #[must_use]
1277 pub fn bibliotecas(&self) -> &[String] {
1278 self.bibliotecas.as_slice()
1279 }
1280
1281 /// Substrate-canonical per-`Caixa` `:exe` universal-axis
1282 /// nix-built-executable-entry-path-list slice-accessor every consumer
1283 /// of the top-level manifest's Binario-executable axis keys off —
1284 /// returns the author-declared `:exe` list verbatim as a `&[String]`
1285 /// slice-view over the same backing buffer the raw
1286 /// `self.exe.as_slice()` field access borrows from. Empty-list-
1287 /// carrying (`:exe` is a default-empty axis every `defcaixa` form
1288 /// supplies with an empty `()` when unset; the [`Self::from_lisp`]
1289 /// derive folds an omitted `:exe` through `#[serde(default)]` to
1290 /// `Vec::new()`, so a `Caixa` past parse definitionally carries a
1291 /// `Vec<String>` slot — possibly empty — and the returned `&[String]`
1292 /// degenerates to an empty slice on that arm without any silent
1293 /// `None` collapse).
1294 ///
1295 /// The `:exe` slot carries the universal-axis nix-built executable
1296 /// entry-path list every `:kind Binario` caixa emits under
1297 /// (CAIXA-SDLC §I — the author-facing surface every `defcaixa`
1298 /// form supplies alongside `:nome` / `:versao` / `:kind`; the
1299 /// substrate-wide `exe/`-directory-fenced entry-carrier axis every
1300 /// downstream flake-build-facing consumer keys off) — the typed
1301 /// slot's `Vec<String>` accept-set (empty-per-entry rejected
1302 /// through [`ManifestError::CodePathEmpty { slot: ":exe" }`],
1303 /// non-sandboxed-relative-shape rejected through
1304 /// [`ManifestError::CodePathShape`], cross-entry duplicate rejected
1305 /// through [`ManifestError::CodePathDuplicate`], out-of-`exe/`-
1306 /// directory paths rejected past the layout's
1307 /// [`crate::LayoutError::ExeOutsideDir`] `starts_with` fence) maps
1308 /// onto every load-bearing downstream consumer the substrate carries
1309 /// — the [`crate::LayoutInvariants`] Binario-arm empty-check +
1310 /// per-entry file-exists + `exe/`-directory-fence loop at
1311 /// caixa-core/src/layout.rs that gates each entry through
1312 /// [`crate::LayoutError::BinarioWithoutExe`] / `MissingEntry` /
1313 /// `ExeOutsideDir`, the compound `has_code` OR-fold on the
1314 /// [`crate::LayoutError::SupervisorOwnsCode`] /
1315 /// [`crate::LayoutError::AplicacaoOwnsCode`] kind-coherence gate
1316 /// that fences code-surface slots off from the two no-code kinds,
1317 /// [`Self::declared_foreign_code_slots`]'s `!self.exe.is_empty()`
1318 /// arm on the [`crate::LayoutError::ForeignCodeSlot`] gate that
1319 /// fences the `:exe` code surface off from every non-Binario code-
1320 /// running kind, [`Self::validate_code_paths`]'s per-slot shape gate
1321 /// that walks each entry through the sandbox-relative / cross-entry
1322 /// duplicate gates, every future per-`Caixa` executable-facing
1323 /// renderer the CAIXA-SDLC §I roadmap acknowledges (the future
1324 /// `caixa-flake` per-Binario `packages.<system>.<nome>` derivation
1325 /// entry the caixa-flake docstring roadmaps, the future per-cluster
1326 /// `nix-store` overlay the M4 CR materializer resolves per-CR, the
1327 /// future `feira nix` per-executable Binario-target emit path).
1328 ///
1329 /// Prior to this lift the `.exe` field was accessed inline at three
1330 /// production sites — the compound-code-path `has_code =
1331 /// !caixa.bibliotecas().is_empty() || !caixa.exe.is_empty() ||
1332 /// !caixa.servicos.is_empty()` OR-fold on the
1333 /// [`crate::LayoutError::SupervisorOwnsCode`] /
1334 /// `AplicacaoOwnsCode` kind-coherence gate, the Binario-arm
1335 /// `caixa.exe.is_empty()` [`crate::LayoutError::BinarioWithoutExe`]
1336 /// gate, the per-entry `for p in &caixa.exe`
1337 /// `MissingEntry`/`ExeOutsideDir` walk, and the
1338 /// [`Self::declared_foreign_code_slots`]'s
1339 /// `!self.exe.is_empty()` arm on the `ForeignCodeSlot` gate — four
1340 /// open-coded field-accesses that expressed no compile-time link
1341 /// back to the typed slot. A future extension of the `:exe` axis
1342 /// to a richer executable surface — a per-`:exe` structured
1343 /// `BinarioEntry { path, wrapper, capabilities }` at the storage
1344 /// layer once the substrate absorbs the per-executable
1345 /// nix-wrapper + linux-capabilities tuple the CAIXA-SDLC §I
1346 /// executable roadmap acknowledges, a per-registry `:exe` allowlist
1347 /// the M4 CR materializer enforces per-CR (the "cluster policy
1348 /// demands every Binario declare an explicit `:wrapper`" arm), a
1349 /// promotion of the plain `Vec<String>` byte-string list to a
1350 /// richer `Vec<ExecutablePath>` newtype discriminated on the
1351 /// `exe/<nome>`-shape grammar the layout's `starts_with(exe_dir)`
1352 /// fence already resolves through — would have had to be threaded
1353 /// through all four open-coded copies in lockstep or the layout
1354 /// gate, the shape validator, and the `feira nix` emit path would
1355 /// silently disagree on which executable paths a given [`Caixa`]
1356 /// resolves to (an author's `:exe ("exe/cli" "exe/serve")` would
1357 /// satisfy layout while `feira nix` silently packaged a drifted
1358 /// other list, or vice versa). Lifting the resolution to a typed
1359 /// method on the substrate primitive means every downstream
1360 /// consumer of the caixa's per-`Caixa` executable-source surface
1361 /// reaches for exactly one typed dispatch — the resolver's accept-
1362 /// set migrates as a unit on any future axis addition.
1363 ///
1364 /// Fourth outer top-level [`Caixa`] `&[T]`-return slice-accessor —
1365 /// folds on the "outer [`Caixa`] `&[T]` slice" projection pattern
1366 /// [`Self::autores`] (b5d813f) opened, [`Self::etiquetas`]
1367 /// (78c7d3c) folded on, and [`Self::bibliotecas`] (8a36c23) closed
1368 /// the universal-axis text-tag family of. Opens the outer-`Caixa`
1369 /// foreign-code-slot `&[T]` sub-family the sibling `:servicos`
1370 /// future lift closes onto (per the trio of code-surface list slots
1371 /// the [`Self::validate_code_paths`] per-slot dispatch tuple
1372 /// already carries — `:bibliotecas` + `:exe` + `:servicos`, of which
1373 /// `:bibliotecas` landed at 8a36c23 and `:servicos` remains as the
1374 /// last unlifted code-surface slot). Sibling in shape to the peer
1375 /// per-`:supervisor` [`crate::supervisor::SupervisorSpec::children`]
1376 /// (bc92bce), per-`:placement`
1377 /// [`crate::aplicacao::Placement::clusters`] (a6e18d7),
1378 /// per-`:membros` [`crate::aplicacao::AplicacaoSpec::membros`]
1379 /// (6c77e36), per-`:contratos`
1380 /// [`crate::aplicacao::AplicacaoSpec::contratos`] (0dcc926), and
1381 /// per-`:upgrade-from :instructions`
1382 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1383 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1384 /// typed-slot list axes, extended here to the outer top-level
1385 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1386 /// `&Vec<String>`) because every downstream consumer of the
1387 /// executable-source list treats it as a read-only sequence — the
1388 /// slice-view is the narrowest borrow that supports every
1389 /// present + roadmapped consumer (`.iter()`, `.len()`,
1390 /// `.is_empty()`) without leaking the backing `Vec`'s
1391 /// grow/push/reserve surface no consumer of the typed view
1392 /// reaches for (the storage-side `Vec` remains reachable through
1393 /// the `pub exe` field for the mutation-carrying serde
1394 /// round-trip and per-test fixture-mutation paths). Named `exe()`
1395 /// to match the storage field's name; the accessor's identity
1396 /// maps onto the canonical CAIXA-SDLC §I vocabulary the slot's
1397 /// docstring already carries.
1398 #[must_use]
1399 pub fn exe(&self) -> &[String] {
1400 self.exe.as_slice()
1401 }
1402
1403 /// Substrate-canonical per-`Caixa` `:servicos` universal-axis
1404 /// ComputeUnit-CR-YAML-entry-path-list slice-accessor every consumer
1405 /// of the top-level manifest's Servico-component axis keys off —
1406 /// returns the author-declared `:servicos` list verbatim as a
1407 /// `&[String]` slice-view over the same backing buffer the raw
1408 /// `self.servicos.as_slice()` field access borrows from. Empty-list-
1409 /// carrying (`:servicos` is a default-empty axis every `defcaixa`
1410 /// form supplies with an empty `()` when unset; the
1411 /// [`Self::from_lisp`] derive folds an omitted `:servicos` through
1412 /// `#[serde(default)]` to `Vec::new()`, so a `Caixa` past parse
1413 /// definitionally carries a `Vec<String>` slot — possibly empty —
1414 /// and the returned `&[String]` degenerates to an empty slice on
1415 /// that arm without any silent `None` collapse).
1416 ///
1417 /// The `:servicos` slot carries the universal-axis
1418 /// `.computeunit.yaml` ComputeUnit-CR entry-path list every
1419 /// `:kind Servico` caixa emits under (CAIXA-SDLC §I — the
1420 /// author-facing surface every `defcaixa` form supplies alongside
1421 /// `:nome` / `:versao` / `:kind`; the substrate-wide
1422 /// `servicos/`-directory-fenced entry-carrier axis every downstream
1423 /// Servico-facing renderer keys off) — the typed slot's
1424 /// `Vec<String>` accept-set (empty-per-entry rejected through
1425 /// [`ManifestError::CodePathEmpty { slot: ":servicos" }`],
1426 /// non-sandboxed-relative-shape rejected through
1427 /// [`ManifestError::CodePathShape`], non-`.computeunit.yaml`
1428 /// extension rejected through
1429 /// [`ManifestError::CodePathNonComputeUnitYamlExtension`], cross-
1430 /// entry duplicate rejected through
1431 /// [`ManifestError::CodePathDuplicate`], `len != 1` rejected by the
1432 /// V0 [`crate::ServicoCountMismatch`] gate on the per-Servico
1433 /// renderer entry-points, out-of-`servicos/`-directory paths
1434 /// rejected past the layout's [`crate::LayoutError::ServicoOutsideDir`]
1435 /// `starts_with` fence) maps onto every load-bearing downstream
1436 /// consumer the substrate carries — the [`crate::LayoutInvariants`]
1437 /// Servico-arm empty-check + per-entry file-exists + `servicos/`-
1438 /// directory-fence loop at caixa-core/src/layout.rs that gates each
1439 /// entry through [`crate::LayoutError::ServicoWithoutServicos`] /
1440 /// `MissingEntry` / `ServicoOutsideDir`, the compound `has_code`
1441 /// OR-fold on the [`crate::LayoutError::SupervisorOwnsCode`] /
1442 /// [`crate::LayoutError::AplicacaoOwnsCode`] kind-coherence gate
1443 /// that fences code-surface slots off from the two no-code kinds,
1444 /// [`Self::declared_foreign_code_slots`]'s
1445 /// `!self.servicos.is_empty()` arm on the
1446 /// [`crate::LayoutError::ForeignCodeSlot`] gate that fences the
1447 /// `:servicos` code surface off from every non-Servico code-running
1448 /// kind, [`Self::validate_code_paths`]'s per-slot shape gate that
1449 /// walks each entry through the sandbox-relative / `.computeunit.
1450 /// yaml`-extension / cross-entry duplicate gates, the
1451 /// [`crate::require_single_servico`] V0 singularity gate every
1452 /// per-Servico renderer entry-point runs through
1453 /// [`crate::require_v0_servico_shape`], the `feira chart` /
1454 /// `feira deploy` per-verb `first_servico_path` walk at
1455 /// caixa-feira/src/cmd/chart.rs that resolves the singleton
1456 /// ComputeUnit-CR file, every future per-`Caixa` Servico-facing
1457 /// renderer the CAIXA-SDLC §I roadmap acknowledges (the future
1458 /// per-Servico OCI packager, the future M4
1459 /// `wasm.pleme.io/v1alpha1/ComputeUnit` CR materializer, the future
1460 /// per-Servico OTel collector-config emit).
1461 ///
1462 /// Prior to this lift the `.servicos` field was accessed inline at
1463 /// five production sites — the compound-code-path `has_code =
1464 /// !caixa.bibliotecas().is_empty() || !caixa.exe().is_empty() ||
1465 /// !caixa.servicos.is_empty()` OR-fold on the
1466 /// [`crate::LayoutError::SupervisorOwnsCode`] /
1467 /// `AplicacaoOwnsCode` kind-coherence gate, the Servico-arm
1468 /// `caixa.servicos.is_empty()`
1469 /// [`crate::LayoutError::ServicoWithoutServicos`] gate, the
1470 /// per-entry `for p in &caixa.servicos`
1471 /// `MissingEntry`/`ServicoOutsideDir` walk, the
1472 /// [`Self::declared_foreign_code_slots`]'s
1473 /// `!self.servicos.is_empty()` arm on the `ForeignCodeSlot` gate,
1474 /// and the [`crate::require_single_servico`] V0 count gate's
1475 /// `caixa.servicos.len() == 1` / `caixa.servicos.len()` count
1476 /// projection (both the accept-arm predicate and the
1477 /// diagnostic-carrying `ServicoCountMismatch { count }`
1478 /// projection) — five open-coded field-accesses across three
1479 /// crates that expressed no compile-time link back to the typed
1480 /// slot. A future extension of the `:servicos` axis to a richer
1481 /// component surface — a per-`:servicos` structured
1482 /// `ServicoEntry { path, world, capabilities }` at the storage
1483 /// layer once the substrate absorbs the per-component WIT-world +
1484 /// capability-set tuple the CAIXA-SDLC §I Servico roadmap
1485 /// acknowledges, a per-registry `:servicos` allowlist the M4 CR
1486 /// materializer enforces per-CR (the "cluster policy demands every
1487 /// Servico declare an explicit `:world`" arm), a promotion of the
1488 /// plain `Vec<String>` byte-string list to a richer
1489 /// `Vec<ComputeUnitPath>` newtype discriminated on the
1490 /// `servicos/<nome>.computeunit.yaml`-shape grammar the layout's
1491 /// `starts_with(servicos_dir)` fence and the
1492 /// [`crate::render::is_computeunit_yaml_extension`] predicate
1493 /// already resolve through, a promotion of the V0 singleton
1494 /// contract to a multi-component `Vec<ComputeUnitPath>` past the M5
1495 /// component-model multi-world boundary — would have had to be
1496 /// threaded through all five open-coded copies in lockstep or the
1497 /// layout gate, the shape validator, the V0 count gate, and the
1498 /// `feira chart` / `feira deploy` entry-point walks would silently
1499 /// disagree on which ComputeUnit-CR paths a given [`Caixa`]
1500 /// resolves to (an author's `:servicos ("servicos/foo.computeunit.
1501 /// yaml")` would satisfy layout while `feira chart` silently
1502 /// packaged a drifted other list, or vice versa). Lifting the
1503 /// resolution to a typed method on the substrate primitive means
1504 /// every downstream consumer of the caixa's per-`Caixa`
1505 /// ComputeUnit-CR-source surface reaches for exactly one typed
1506 /// dispatch — the resolver's accept-set migrates as a unit on any
1507 /// future axis addition.
1508 ///
1509 /// Fifth and final outer top-level [`Caixa`] `&[T]`-return slice-
1510 /// accessor — folds on the "outer [`Caixa`] `&[T]` slice"
1511 /// projection pattern [`Self::autores`] (b5d813f) opened,
1512 /// [`Self::etiquetas`] (78c7d3c) folded on, [`Self::bibliotecas`]
1513 /// (8a36c23) closed the universal-axis text-tag family of, and
1514 /// [`Self::exe`] (65d9527) opened the foreign-code-slot sub-family
1515 /// of. Closes the outer-`Caixa` foreign-code-slot `&[T]` sub-family
1516 /// — with `:bibliotecas`, `:exe`, and `:servicos` now each carrying
1517 /// a substrate-canonical slice accessor, the trio of code-surface
1518 /// list slots the [`Self::validate_code_paths`] per-slot dispatch
1519 /// tuple carries is complete on the typed dispatch surface (the
1520 /// internal `[(":bibliotecas", &self.bibliotecas, ..), (":exe",
1521 /// &self.exe, ..), (":servicos", &self.servicos, ..)]` per-slot
1522 /// dispatch tuple's homogeneous `&Vec<String>`-typed shape blocks a
1523 /// per-element accessor swap in isolation — a future companion lift
1524 /// promotes the tuple's element type to `&[String]` and threads the
1525 /// triple of typed dispatches through as a unit). Sibling in shape
1526 /// to the peer per-`:supervisor`
1527 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
1528 /// per-`:placement` [`crate::aplicacao::Placement::clusters`]
1529 /// (a6e18d7), per-`:membros`
1530 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
1531 /// per-`:contratos`
1532 /// [`crate::aplicacao::AplicacaoSpec::contratos`] (0dcc926), and
1533 /// per-`:upgrade-from :instructions`
1534 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1535 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1536 /// typed-slot list axes, extended here to the outer top-level
1537 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1538 /// `&Vec<String>`) because every downstream consumer of the
1539 /// ComputeUnit-CR-source list treats it as a read-only sequence —
1540 /// the slice-view is the narrowest borrow that supports every
1541 /// present + roadmapped consumer (`.iter()`, `.len()`,
1542 /// `.is_empty()`, `.first()`) without leaking the backing `Vec`'s
1543 /// grow/push/reserve surface no consumer of the typed view reaches
1544 /// for (the storage-side `Vec` remains reachable through the
1545 /// `pub servicos` field for the mutation-carrying serde round-trip
1546 /// and per-test fixture-mutation paths, and for the
1547 /// [`Self::validate_code_paths`] per-slot dispatch tuple whose
1548 /// homogeneous-element-type shape carries the raw field access
1549 /// until the trio-closure lift promotes the tuple as a unit).
1550 /// Named `servicos()` to match the storage field's name; the
1551 /// accessor's identity maps onto the canonical CAIXA-SDLC §I
1552 /// vocabulary the slot's docstring already carries.
1553 #[must_use]
1554 pub fn servicos(&self) -> &[String] {
1555 self.servicos.as_slice()
1556 }
1557
1558 /// Substrate-canonical per-`Caixa` `:deps` universal-axis
1559 /// runtime-dependency-declaration-list slice-accessor every consumer
1560 /// of the top-level manifest's runtime-dep-graph axis keys off —
1561 /// returns the author-declared `:deps` list verbatim as a `&[Dep]`
1562 /// slice-view over the same backing buffer the raw
1563 /// `self.deps.as_slice()` field access borrows from. Empty-list-
1564 /// carrying (`:deps` is a default-empty axis every `defcaixa` form
1565 /// supplies with an empty `()` when unset; the [`Self::from_lisp`]
1566 /// derive folds an omitted `:deps` through `#[serde(default)]` to
1567 /// `Vec::new()`, so a `Caixa` past parse definitionally carries a
1568 /// `Vec<Dep>` slot — possibly empty — and the returned `&[Dep]`
1569 /// degenerates to an empty slice on that arm without any silent
1570 /// `None` collapse).
1571 ///
1572 /// The `:deps` slot carries the universal-axis runtime dependency
1573 /// list every kind of caixa emits under (CAIXA-SDLC §I — the author-
1574 /// facing surface every `defcaixa` form supplies alongside `:nome` /
1575 /// `:versao` / `:kind`; the substrate-wide runtime-closure-input axis
1576 /// every downstream resolver-facing artifact emits under) — the
1577 /// typed slot's `Vec<Dep>` accept-set (empty-`:nome` rejected through
1578 /// [`DepError::NomeEmpty`], non-DNS-1123-label `:nome` rejected
1579 /// through [`DepError::NomeInvalid`], malformed `:versao` rejected
1580 /// through [`DepError::VersaoInvalid`], empty `:fonte.repo` rejected
1581 /// through [`DepError::FonteRepoEmpty`], within-list duplicate `:nome`
1582 /// rejected through [`DepError::DuplicateNome { list: ":deps" }`])
1583 /// maps onto every load-bearing downstream consumer the substrate
1584 /// carries — the [`Self::validate_deps`] per-entry
1585 /// [`Dep::validate`] + within-list dedup walk at
1586 /// caixa-core/src/manifest.rs, the [`crate::dep::validate_no_self_dep`]
1587 /// cross-list self-reference gate at caixa-core/src/layout.rs that
1588 /// checks each entry against the caixa's own `:nome`, the
1589 /// caixa-resolver `for dep in &root.deps` closure walk at
1590 /// caixa-resolver/src/resolve.rs that seeds every git-clone target
1591 /// through the resolver's [`crate::Dep`]-keyed pipeline, the
1592 /// caixa-crd `caixa.deps.iter().map(dep_into_ref).collect()` fold at
1593 /// caixa-crd/src/conversion.rs that materializes each entry into the
1594 /// K8s `Caixa` CR's `spec.deps` field, every future per-`Caixa`
1595 /// resolver-facing renderer the CAIXA-SDLC §I roadmap acknowledges
1596 /// (the future per-cluster runtime-closure-audit overlay the M4 CR
1597 /// materializer resolves per-CR, the future `lacre.lisp` BLAKE3-
1598 /// closure emit walk the caixa-resolver docstring roadmaps).
1599 ///
1600 /// First outer top-level [`Caixa`] `&[Dep]`-return slice-accessor —
1601 /// opens the outer-`Caixa` dependency-slot `&[Dep]` sub-family the
1602 /// sibling `:deps-dev` future lift closes on. Peer of the closed
1603 /// outer-`Caixa` foreign-code-slot `&[String]` sub-family
1604 /// ([`Self::bibliotecas`] 8a36c23, [`Self::exe`] 65d9527,
1605 /// [`Self::servicos`] 611f78b) and the outer-`Caixa` universal-axis
1606 /// text-tag family ([`Self::autores`] b5d813f, [`Self::etiquetas`]
1607 /// 78c7d3c) — extends the "outer [`Caixa`] `&[T]` slice" projection
1608 /// pattern onto a novel element-type axis (`Dep` composite vs the
1609 /// prior sibling family's `String` scalar). Sibling in shape to the
1610 /// peer per-`:supervisor`
1611 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
1612 /// per-`:placement` [`crate::aplicacao::Placement::clusters`]
1613 /// (a6e18d7), per-`:membros`
1614 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
1615 /// per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
1616 /// (0dcc926), and per-`:upgrade-from :instructions`
1617 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1618 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1619 /// typed-slot list axes, extended here to the outer top-level
1620 /// [`Caixa`] universal-axis dep-graph surface. Returns `&[Dep]`
1621 /// (not `&Vec<Dep>`) because every downstream consumer of the
1622 /// runtime-dep list treats it as a read-only sequence — the slice-
1623 /// view is the narrowest borrow that supports every present +
1624 /// roadmapped consumer (`.iter()`, `.len()`, `.is_empty()`) without
1625 /// leaking the backing `Vec`'s grow/push/reserve surface no consumer
1626 /// of the typed view reaches for (the storage-side `Vec` remains
1627 /// reachable through the `pub deps` field for the mutation-carrying
1628 /// serde round-trip and per-test fixture-mutation paths). Named
1629 /// `deps()` to match the storage field's name; the accessor's
1630 /// identity maps onto the canonical CAIXA-SDLC §I vocabulary the
1631 /// slot's docstring already carries.
1632 #[must_use]
1633 pub fn deps(&self) -> &[Dep] {
1634 self.deps.as_slice()
1635 }
1636
1637 /// Substrate-canonical per-`Caixa` `:deps-dev` universal-axis
1638 /// development-only-dependency-declaration-list slice-accessor every
1639 /// consumer of the top-level manifest's dev-dep-graph axis keys off —
1640 /// returns the author-declared `:deps-dev` list verbatim as a `&[Dep]`
1641 /// slice-view over the same backing buffer the raw
1642 /// `self.deps_dev.as_slice()` field access borrows from. Empty-list-
1643 /// carrying (`:deps-dev` is a default-empty axis every `defcaixa`
1644 /// form supplies with an empty `()` when unset; the
1645 /// [`Self::from_lisp`] derive folds an omitted `:deps-dev` through
1646 /// `#[serde(default)]` to `Vec::new()`, so a `Caixa` past parse
1647 /// definitionally carries a `Vec<Dep>` slot — possibly empty — and
1648 /// the returned `&[Dep]` degenerates to an empty slice on that arm
1649 /// without any silent `None` collapse).
1650 ///
1651 /// The `:deps-dev` slot carries the universal-axis dev-only
1652 /// dependency list every kind of caixa emits under (CAIXA-SDLC §I —
1653 /// the author-facing sibling of `:deps` that every `defcaixa` form
1654 /// supplies to declare tests / lint / bench closures the runtime
1655 /// `:deps` axis does not carry; the substrate-wide dev-closure-input
1656 /// axis every downstream test-facing artifact emits under, matching
1657 /// Cargo's `[dev-dependencies]` table's dev-time-only visibility
1658 /// contract) — the typed slot's `Vec<Dep>` accept-set (empty-`:nome`
1659 /// rejected through [`DepError::NomeEmpty`], non-DNS-1123-label
1660 /// `:nome` rejected through [`DepError::NomeInvalid`], malformed
1661 /// `:versao` rejected through [`DepError::VersaoInvalid`], empty
1662 /// `:fonte.repo` rejected through [`DepError::FonteRepoEmpty`],
1663 /// within-list duplicate `:nome` rejected through
1664 /// [`DepError::DuplicateNome { list: ":deps-dev" }`]) maps onto every
1665 /// load-bearing downstream consumer the substrate carries — the
1666 /// [`Self::validate_deps`] per-entry [`Dep::validate`] + within-list
1667 /// dedup walk at caixa-core/src/manifest.rs, the
1668 /// [`crate::dep::validate_no_self_dep`] cross-list self-reference
1669 /// gate at caixa-core/src/layout.rs that checks each entry against
1670 /// the caixa's own `:nome`, the caixa-resolver
1671 /// `for dep in &root.deps_dev` closure walk at
1672 /// caixa-resolver/src/resolve.rs that seeds every dev-only git-clone
1673 /// target through the resolver's [`crate::Dep`]-keyed pipeline, and
1674 /// every future per-`Caixa` resolver-facing renderer the CAIXA-SDLC
1675 /// §I roadmap acknowledges (the future per-cluster dev-closure-audit
1676 /// overlay the M4 CR materializer resolves per-CR, the future
1677 /// `lacre.lisp` BLAKE3-closure emit walk the caixa-resolver docstring
1678 /// roadmaps).
1679 ///
1680 /// Second outer top-level [`Caixa`] `&[Dep]`-return slice-accessor —
1681 /// closes the outer-`Caixa` dependency-slot `&[Dep]` sub-family the
1682 /// sibling [`Self::deps`] (ad34b4e) opened on. The two accessors
1683 /// jointly close the two-list dep-graph surface every downstream
1684 /// resolver-facing consumer keys off (runtime `:deps` +
1685 /// dev-only `:deps-dev`, the canonical Cargo-shaped dependency-table
1686 /// pair the [`Self::validate_deps`] gate already walks in canonical
1687 /// order). Peer of the closed outer-`Caixa` foreign-code-slot
1688 /// `&[String]` sub-family ([`Self::bibliotecas`] 8a36c23,
1689 /// [`Self::exe`] 65d9527, [`Self::servicos`] 611f78b) and the outer-
1690 /// `Caixa` universal-axis text-tag family ([`Self::autores`]
1691 /// b5d813f, [`Self::etiquetas`] 78c7d3c) — folds the "outer
1692 /// [`Caixa`] `&[T]` slice" projection pattern onto the sibling
1693 /// dev-dep composite-element axis (`Dep` composite, matching the
1694 /// [`Self::deps`] element type). Sibling in shape to the peer
1695 /// per-`:supervisor` [`crate::supervisor::SupervisorSpec::children`]
1696 /// (bc92bce), per-`:placement`
1697 /// [`crate::aplicacao::Placement::clusters`] (a6e18d7),
1698 /// per-`:membros` [`crate::aplicacao::AplicacaoSpec::membros`]
1699 /// (6c77e36), per-`:contratos`
1700 /// [`crate::aplicacao::AplicacaoSpec::contratos`] (0dcc926), and
1701 /// per-`:upgrade-from :instructions`
1702 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1703 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1704 /// typed-slot list axes, folded here to the outer top-level
1705 /// [`Caixa`] universal-axis dev-dep-graph surface. Returns `&[Dep]`
1706 /// (not `&Vec<Dep>`) because every downstream consumer of the
1707 /// dev-dep list treats it as a read-only sequence — the slice-view
1708 /// is the narrowest borrow that supports every present +
1709 /// roadmapped consumer (`.iter()`, `.len()`, `.is_empty()`) without
1710 /// leaking the backing `Vec`'s grow/push/reserve surface no consumer
1711 /// of the typed view reaches for (the storage-side `Vec` remains
1712 /// reachable through the `pub deps_dev` field for the mutation-
1713 /// carrying serde round-trip and per-test fixture-mutation paths).
1714 /// Named `deps_dev()` to match the storage field's `snake_case` name;
1715 /// the kebab-case author-surface tag `:deps-dev` is the same axis
1716 /// after tatara-lisp's kebab↔snake fold and the accessor's identity
1717 /// maps onto the canonical CAIXA-SDLC §I vocabulary the slot's
1718 /// docstring already carries.
1719 #[must_use]
1720 pub fn deps_dev(&self) -> &[Dep] {
1721 self.deps_dev.as_slice()
1722 }
1723
1724 /// Substrate-canonical per-`Caixa` `:limits` M2 typed-slot outer-
1725 /// composite Lunatic-per-process wasm32-sandboxing-composite optional-
1726 /// composite-reference accessor every consumer of the top-level
1727 /// manifest's per-Servico [`LimitsSpec`] outer-composite reader keys
1728 /// off — returns the author-declared `:limits` typed composite
1729 /// verbatim as an `Option<&LimitsSpec>` reference over the same
1730 /// backing storage the raw `self.limits.as_ref()` field access
1731 /// borrows from, with `None` naming the "no `:limits` block
1732 /// authored — every per-axis Lunatic-sandbox cap defers to the
1733 /// wasm-engine-default arm named on the per-axis
1734 /// [`LimitsSpec::memory`] / [`LimitsSpec::fuel`] /
1735 /// [`LimitsSpec::wall_clock`] / [`LimitsSpec::cpu`] scalar-accessor
1736 /// docstrings" partition every downstream Servico-M2-overlay
1737 /// emitter treats as "emit nothing" and the sibling
1738 /// [`crate::StandardLayout::verify`] per-`:limits` shape gate
1739 /// treats as "skip the per-axis
1740 /// [`crate::LimitsError::MemoryZero`] / `MemoryBelowWasm32Page` /
1741 /// `FuelZero` / `WallClockZero` / `CpuZero` refusal cascade".
1742 ///
1743 /// The outer `:limits` slot carries the M2 Servico-runtime typed
1744 /// composite — the load-bearing container of every Lunatic-shaped
1745 /// per-process wasm32-sandbox cap axis every long-running wasm
1746 /// component's runtime dispatches on (INSPIRATIONS §III.1 —
1747 /// Lunatic per-process linear-memory / fuel / wall-clock /
1748 /// millicore cap primitives translated onto pleme-io's typed
1749 /// `:limits :memory` / `:limits :fuel` / `:limits :wall-clock` /
1750 /// `:limits :cpu` sub-slot axes; CAIXA-SDLC §II — the typed-M2
1751 /// slot algebra the wasm-engine + `pleme-computeunit` Helm-library
1752 /// chart both fan on). Every per-`:limits` axis threads through a
1753 /// lifted per-slot accessor on the [`LimitsSpec`] type: the
1754 /// [`LimitsSpec::memory`] wasm32 linear-memory byte-cap scalar
1755 /// accessor, the [`LimitsSpec::fuel`] wasmtime fuel-cap scalar
1756 /// accessor, the [`LimitsSpec::wall_clock`] per-call wall-clock
1757 /// deadline scalar accessor, and the [`LimitsSpec::cpu`]
1758 /// K8s-millicore soft-CPU-share scalar accessor. Every downstream
1759 /// consumer that reaches for a limits axis first passes through
1760 /// this outer accessor onto the composite and then dispatches
1761 /// onto the per-axis accessor — the two-level dispatch means
1762 /// every per-`:limits` reader now routes through a typed dispatch
1763 /// on the substrate primitive at both altitudes.
1764 ///
1765 /// Prior to this lift the `.limits` `Option<LimitsSpec>` composite
1766 /// was accessed inline at three production sites — the
1767 /// [`crate::StandardLayout::verify`] per-`:limits` shape gate's
1768 /// `if let Some(l) = &caixa.limits { … }` traversal head
1769 /// (caixa-core/src/layout.rs:882, which drives the per-axis
1770 /// refusal cascade on the composite: the `LimitsError::MemoryZero`
1771 /// / `MemoryBelowWasm32Page` / `MemoryExceedsWasm32Max` /
1772 /// `FuelZero` / `FuelExceedsMax` / `WallClockZero` /
1773 /// `WallClockExceedsMax` / `CpuZero` / `CpuExceedsMax` refusals
1774 /// [`LimitsSpec::validate`] fans onto), the
1775 /// [`crate::render::servico_m2_overlay`] per-Servico M2 overlay
1776 /// emitter's `if let Some(limits) = &caixa.limits { … }` traversal
1777 /// head (caixa-core/src/render.rs:18504, which drives the
1778 /// `M2_KEY_LIMITS`-keyed `limits.is_empty()`-gated `serde_yaml`
1779 /// projection every `caixa-helm` / `caixa-flux` Servico values-
1780 /// block emitter fans on), and the
1781 /// [`Self::declared_servico_slots`] per-Servico M2 declared-slot-
1782 /// set enumerator's `self.limits.is_some()` presence probe
1783 /// (caixa-core/src/manifest.rs:1788, which drives the
1784 /// `M2_AUTHOR_KEY_LIMITS` kebab-case author-label push every
1785 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] kind-coherence
1786 /// gate reads) — three open-coded outer-field accesses that
1787 /// expressed no compile-time link back to the typed slot at the
1788 /// [`Caixa`] altitude. A future extension of the `:limits` outer
1789 /// axis to a richer author surface (a multi-`:limits` list the M4
1790 /// CR materializer resolves per-CR at admission time so a Servico
1791 /// can expose a compute-heavy + IO-heavy limits pair, a per-
1792 /// cluster `:limits-overrides` slot the operator pins so a
1793 /// cluster-specific policy can tighten a caixa-declared cap
1794 /// without re-authoring the `caixa.lisp`, a promotion of the
1795 /// plain `Option<LimitsSpec>` to a richer
1796 /// `{static, dynamic}` partition once the wasm-engine's runtime-
1797 /// resolved dynamic-cap surface lands) would have had to be
1798 /// threaded through all three open-coded copies in lockstep or
1799 /// one consumer would silently disagree with the peers on which
1800 /// limits composite a given Caixa resolves to — the layout gate's
1801 /// per-axis bracket-dispatch seed reading the raw slot while the
1802 /// peer `servico_m2_overlay` emitter read an operator-resolved
1803 /// slot would silently split the build-time sandbox-shape gate
1804 /// from the runtime `ComputeUnit` CR emission gate, a three-
1805 /// consumer split at the layout gate, the M2 overlay emitter, and
1806 /// the declared-slot enumerator far from the source `caixa.lisp`
1807 /// with no field naming the limits-drift root cause. Lifting the
1808 /// resolution rule to a typed method on the substrate primitive
1809 /// means every downstream consumer of the caixa's per-`Caixa`
1810 /// Lunatic-sandboxing outer-composite surface reaches for exactly
1811 /// one typed dispatch — the resolver's accept-set migrates as a
1812 /// unit on any future axis addition.
1813 ///
1814 /// First outer top-level [`Caixa`] `Option<&Composite>`-return
1815 /// composite-reference accessor — opens the outer-`Caixa`
1816 /// `Option<&Composite>` composite-reference projection pattern the
1817 /// sibling per-`Caixa` `:behavior` [`crate::BehaviorSpec`] /
1818 /// `:politicas` [`crate::aplicacao::MeshPolicy`] / `:placement`
1819 /// [`crate::aplicacao::Placement`] / `:entrada`
1820 /// [`crate::aplicacao::Entrada`] future outer-composite lifts
1821 /// fold on. Peer of the M3 mesh-slot outer-composite family the
1822 /// sibling [`crate::AplicacaoSpec::politicas`] (534dc21) /
1823 /// [`crate::AplicacaoSpec::placement`] (9abb8f0) /
1824 /// [`crate::AplicacaoSpec::entrada`] (d32111c) composite-reference
1825 /// accessors already close on the outer [`crate::AplicacaoSpec`]
1826 /// altitude — extends that "one typed dispatch on the substrate
1827 /// primitive, thin projections at each consumer" discipline onto
1828 /// the outer top-level [`Caixa`] altitude, opening the M2 Servico-
1829 /// runtime slot family's outer-composite axis. Returns
1830 /// `Option<&LimitsSpec>` (not the owning composite by copy or
1831 /// clone) because every downstream consumer of the limits
1832 /// composite treats it as a read-only per-axis dispatch source —
1833 /// the reference-view is the narrowest borrow that supports every
1834 /// present + roadmapped consumer (per-axis accessor dispatch,
1835 /// `.is_empty()`-gated overlay projection, presence-probe early
1836 /// return on the "author-omitted `:limits` ⇒ engine-default
1837 /// applies" partition) without cloning the composite through
1838 /// every consumer's fast path. The `Option` half of the return-
1839 /// type preserves the load-bearing "author-omitted `:limits` ⇒
1840 /// engine-default applies" partition (not a default composite the
1841 /// downstream must reject on emptiness) — the accessor projects
1842 /// the raw `Option<LimitsSpec>` slot's presence bit through the
1843 /// reference-return unchanged. Named `limits()` to match the
1844 /// storage field's name verbatim and the tatara-lisp author-
1845 /// surface term (`:limits`) the field's own docstring already
1846 /// carries.
1847 #[must_use]
1848 pub fn limits(&self) -> Option<&LimitsSpec> {
1849 self.limits.as_ref()
1850 }
1851
1852 /// Substrate-canonical per-`Caixa` `:behavior` M2 typed-slot outer-
1853 /// composite OTP-`gen_server`-shaped callback-table optional-
1854 /// composite-reference accessor every consumer of the top-level
1855 /// manifest's per-Servico [`BehaviorSpec`] outer-composite reader
1856 /// keys off — returns the author-declared `:behavior` typed
1857 /// composite verbatim as an `Option<&BehaviorSpec>` reference over
1858 /// the same backing storage the raw `self.behavior.as_ref()` field
1859 /// access borrows from, with `None` naming the "no `:behavior`
1860 /// block authored — every per-callback OTP-shaped hook defers to
1861 /// the wasm-engine's runtime default arm named on the per-axis
1862 /// [`BehaviorSpec::on_init`] / [`BehaviorSpec::on_call`] /
1863 /// [`BehaviorSpec::on_cast`] / [`BehaviorSpec::on_info`] /
1864 /// [`BehaviorSpec::on_state_change`] /
1865 /// [`BehaviorSpec::on_terminate`] scalar-accessor docstrings"
1866 /// partition every downstream Servico-M2-overlay emitter treats as
1867 /// "emit nothing" and the sibling [`crate::StandardLayout::verify`]
1868 /// per-`:behavior` shape gate treats as "skip the per-arm
1869 /// [`crate::behavior::BehaviorError`] refusal cascade + the
1870 /// per-callback on-disk `MissingEntry` existence check".
1871 ///
1872 /// The outer `:behavior` slot carries the M2 Servico-runtime typed
1873 /// composite — the load-bearing container of every OTP-shaped
1874 /// per-Servico lifecycle-callback path axis every long-running wasm
1875 /// component's runtime dispatches on (INSPIRATIONS §II.3 — Erlang/
1876 /// OTP `gen_server:init/1` / `handle_call/3` / `handle_cast/2` /
1877 /// `handle_info/2` / `code_change/3` / `terminate/2` primitives
1878 /// translated onto pleme-io's typed `:behavior :on-init` /
1879 /// `:on-call` / `:on-cast` / `:on-info` / `:on-state-change` /
1880 /// `:on-terminate` sub-slot axes; CAIXA-SDLC §II — the typed-M2
1881 /// slot algebra the wasm-engine + `pleme-computeunit` Helm-library
1882 /// chart both fan on). Every per-`:behavior` axis threads through a
1883 /// lifted per-callback accessor on the [`BehaviorSpec`] type
1884 /// (9b4ecde / d66c702 / 156ddbe / 99616ac / 4846cef / 701add7).
1885 /// Every downstream consumer that reaches for a behavior axis
1886 /// first passes through this outer accessor onto the composite
1887 /// and then dispatches onto the per-callback accessor — the
1888 /// two-level dispatch means every per-`:behavior` reader now
1889 /// routes through a typed dispatch on the substrate primitive at
1890 /// both altitudes.
1891 ///
1892 /// Composes cross-slot with the M2 `:upgrade-from` gate: the
1893 /// [`crate::upgrade::validate_upgrade_from_against_behavior`]
1894 /// cross-slot composition gate at [`crate::StandardLayout::verify`]
1895 /// keys the "per-version `:state-change` instruction must have a
1896 /// `:on-state-change` callback" precondition off this accessor's
1897 /// composite (the callback-side counterpart to the
1898 /// `:upgrade-from :instructions :state-change :script` refusal at
1899 /// the appup-side). Threading that gate's traversal input through
1900 /// this accessor closes the cross-slot invariant on the substrate
1901 /// primitive, not on the raw field.
1902 ///
1903 /// Prior to this lift the `.behavior` `Option<BehaviorSpec>`
1904 /// composite was accessed inline at four production sites — the
1905 /// [`crate::StandardLayout::verify`] per-`:behavior` shape gate's
1906 /// `if let Some(b) = &caixa.behavior { … }` traversal head
1907 /// (caixa-core/src/layout.rs:896, which drives the per-arm
1908 /// `BehaviorError` refusal cascade + the per-callback on-disk
1909 /// [`crate::LayoutError::MissingEntry`] existence check under
1910 /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK`]),
1911 /// the [`crate::upgrade::validate_upgrade_from_against_behavior`]
1912 /// cross-slot composition gate's `caixa.behavior.as_ref()`
1913 /// traversal-input feed (caixa-core/src/layout.rs:1008, which
1914 /// drives the `:state-change` ↔ `:on-state-change` precondition
1915 /// refusal), the [`crate::render::servico_m2_overlay`] per-Servico
1916 /// M2 overlay emitter's `if let Some(behavior) = &caixa.behavior
1917 /// { … }` traversal head (caixa-core/src/render.rs:18513, which
1918 /// drives the `M2_KEY_BEHAVIOR`-keyed `behavior.is_empty()`-gated
1919 /// `serde_yaml` projection every `caixa-helm` / `caixa-flux`
1920 /// Servico values-block emitter fans on), and the
1921 /// [`Self::declared_servico_slots`] per-Servico M2 declared-slot-
1922 /// set enumerator's `self.behavior.is_some()` presence probe
1923 /// (caixa-core/src/manifest.rs:1919, which drives the
1924 /// `M2_AUTHOR_KEY_BEHAVIOR` kebab-case author-label push every
1925 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] kind-coherence
1926 /// gate reads) — four open-coded outer-field accesses that
1927 /// expressed no compile-time link back to the typed slot at the
1928 /// [`Caixa`] altitude. A future extension of the `:behavior`
1929 /// outer axis to a richer author surface (a per-callback overlay
1930 /// resolver the operator materializes at admission time so a
1931 /// cluster-specific policy can inject a per-callback tracing
1932 /// interceptor without re-authoring the `caixa.lisp`, a promotion
1933 /// of the plain `Option<BehaviorSpec>` to a richer `{static,
1934 /// dynamic}` partition once a runtime-resolved behavior-swap
1935 /// surface lands, the M4 per-callback middleware chain the
1936 /// caixa-operator's per-Servico admission webhook keys off) would
1937 /// have had to be threaded through all four open-coded copies in
1938 /// lockstep or one consumer would silently disagree with the
1939 /// peers on which behavior composite a given Caixa resolves to —
1940 /// the layout gate's per-callback existence-check seed reading
1941 /// the raw slot while the peer `servico_m2_overlay` emitter read
1942 /// an operator-resolved slot would silently split the build-time
1943 /// callback-shape gate from the runtime `ComputeUnit` CR emission
1944 /// gate from the cross-slot `:state-change` composition gate from
1945 /// the M2 declared-slot enumerator, a four-consumer split far
1946 /// from the source `caixa.lisp` with no field naming the
1947 /// behavior-drift root cause. Lifting the resolution rule to a
1948 /// typed method on the substrate primitive means every downstream
1949 /// consumer of the caixa's per-`Caixa` OTP-callback-table outer-
1950 /// composite surface reaches for exactly one typed dispatch — the
1951 /// resolver's accept-set migrates as a unit on any future axis
1952 /// addition.
1953 ///
1954 /// Second outer top-level [`Caixa`] `Option<&Composite>`-return
1955 /// composite-reference accessor — sibling to the opening
1956 /// [`Self::limits`] (b2bd9d7) accessor on the outer-`Caixa`
1957 /// `Option<&Composite>` composite-reference sub-family, extends
1958 /// the "one typed dispatch on the substrate primitive, thin
1959 /// projections at each consumer" discipline onto the second of
1960 /// the three M2 Servico-runtime slots. The remaining
1961 /// `Option<&Composite>` axes at the outer top-level [`Caixa`]
1962 /// altitude — the M3 mesh-slot family (`:politicas`,
1963 /// `:placement`, `:entrada` — already closed on the inner
1964 /// [`crate::AplicacaoSpec`] altitude via 534dc21 / 9abb8f0 /
1965 /// d32111c) — remain the future sibling lifts on the outer
1966 /// top-level projection. Returns `Option<&BehaviorSpec>` (not
1967 /// the owning composite by copy or clone) because every
1968 /// downstream consumer of the behavior composite treats it as a
1969 /// read-only per-callback dispatch source — the reference-view is
1970 /// the narrowest borrow that supports every present + roadmapped
1971 /// consumer (per-callback accessor dispatch, `.is_empty()`-gated
1972 /// overlay projection, presence-probe early return on the
1973 /// "author-omitted `:behavior` ⇒ runtime-default applies"
1974 /// partition, cross-slot `:state-change` composition input)
1975 /// without cloning the composite through every consumer's fast
1976 /// path. The `Option` half of the return-type preserves the
1977 /// load-bearing "author-omitted `:behavior` ⇒ runtime-default
1978 /// applies" partition (not a default composite the downstream
1979 /// must reject on emptiness) — the accessor projects the raw
1980 /// `Option<BehaviorSpec>` slot's presence bit through the
1981 /// reference-return unchanged. Named `behavior()` to match the
1982 /// storage field's name verbatim and the tatara-lisp author-
1983 /// surface term (`:behavior`) the field's own docstring already
1984 /// carries.
1985 #[must_use]
1986 pub fn behavior(&self) -> Option<&crate::BehaviorSpec> {
1987 self.behavior.as_ref()
1988 }
1989
1990 /// Substrate-canonical per-`Caixa` `:politicas` M3 mesh-slot outer-
1991 /// composite MESH-COMPOSITION-shaped mesh-policy optional-composite-
1992 /// reference accessor every consumer of the top-level manifest's
1993 /// per-Aplicacao [`crate::aplicacao::MeshPolicy`] outer-composite
1994 /// reader keys off — returns the author-declared `:politicas` typed
1995 /// composite verbatim as an `Option<&MeshPolicy>` reference over the
1996 /// same backing storage the raw `self.politicas.as_ref()` field
1997 /// access borrows from, with `None` naming the "no `:politicas`
1998 /// block authored — every per-axis mesh-policy scalar defers to the
1999 /// cluster-default arm named on the per-axis
2000 /// [`crate::aplicacao::MeshPolicy::timeout`] /
2001 /// [`crate::aplicacao::MeshPolicy::retries`] /
2002 /// [`crate::aplicacao::MeshPolicy::circuit_breaker`] /
2003 /// [`crate::aplicacao::MeshPolicy::mtls_required`] /
2004 /// [`crate::aplicacao::MeshPolicy::rate_limit`] scalar-accessor
2005 /// docstrings" partition every downstream caixa-mesh /
2006 /// caixa-flux / caixa-helm Aplicacao-artifact emitter treats as
2007 /// "emit no per-`:politicas` overlay" and the sibling
2008 /// [`Self::aplicacao_view`] Aplicacao-composition seed folds through
2009 /// the [`crate::aplicacao::MeshPolicy::default`] cluster-default
2010 /// arm.
2011 ///
2012 /// The outer `:politicas` slot carries the M3 mesh-slot per-
2013 /// Aplicacao typed composite — the load-bearing container of every
2014 /// mesh-level policy axis every Cilium NetworkPolicy / Gateway API
2015 /// v1.x HTTPRoute / future M4 per-edge policy overlay emitter fans
2016 /// on (MESH-COMPOSITION §III.2 — the Aplicacao's typed mesh-policy
2017 /// composite; §V — the "no infinite blocking" per-call deadline +
2018 /// "sandboxing-by-default" mTLS-enforcement CSE invariants; §III.3
2019 /// — the typed inter-Servico contrato-edge overlay the per-`(:de,
2020 /// :para)` mesh renderer keys off). Every per-`:politicas` axis
2021 /// threads through a lifted per-slot accessor on the
2022 /// [`crate::aplicacao::MeshPolicy`] type: the
2023 /// [`crate::aplicacao::MeshPolicy::mtls_required`] (c0110f1) Cilium
2024 /// mTLS-enforcement toggle, the
2025 /// [`crate::aplicacao::MeshPolicy::retries`] (bdfb399) transient-
2026 /// failure retry budget, the [`crate::aplicacao::MeshPolicy::timeout`]
2027 /// (7073d0f) Gateway-API per-call deadline, the
2028 /// [`crate::aplicacao::MeshPolicy::circuit_breaker`] (b0e741a)
2029 /// Envoy-outlier-detection composite. Every downstream consumer
2030 /// that reaches for a mesh-policy axis first passes through this
2031 /// outer accessor onto the composite and then dispatches onto the
2032 /// per-axis accessor — the two-level dispatch means every per-
2033 /// `:politicas` reader now routes through a typed dispatch on the
2034 /// substrate primitive at both altitudes.
2035 ///
2036 /// Composes through [`Self::aplicacao_view`]'s Aplicacao-composition
2037 /// seed: the Aplicacao-view builder folds the outer `Option`'s
2038 /// author-omitted arm onto the [`crate::aplicacao::MeshPolicy::default`]
2039 /// cluster-default, so the peer inner [`crate::AplicacaoSpec::politicas`]
2040 /// (534dc21) `&MeshPolicy`-return accessor observes a typed
2041 /// composite whether or not the author declared the outer slot.
2042 /// The outer accessor preserves the "author-omitted vs authored-
2043 /// empty" partition the inner accessor's `is_empty()`-gated
2044 /// renderer overlay collapses — routing the presence bit through
2045 /// this accessor keeps the [`Self::declared_mesh_slots`] M3 kind-
2046 /// coherence enumerator's `M3_AUTHOR_KEY_POLITICAS` push separate
2047 /// from the inner `MeshPolicy::is_empty()`-gated overlay elision.
2048 ///
2049 /// Prior to this lift the `.politicas` `Option<MeshPolicy>`
2050 /// composite was accessed inline at two production sites — the
2051 /// [`Self::aplicacao_view`] Aplicacao-composition seed's
2052 /// `self.politicas.clone().unwrap_or_default()` traversal head
2053 /// (caixa-core/src/manifest.rs:1899, which drives the fold onto
2054 /// the [`crate::aplicacao::MeshPolicy::default`] cluster-default
2055 /// arm the inner [`crate::AplicacaoSpec::politicas`] accessor
2056 /// then observes), and the [`Self::declared_mesh_slots`] M3
2057 /// declared-slot-set enumerator's `self.politicas.is_some()`
2058 /// presence probe (caixa-core/src/manifest.rs:1961, which drives
2059 /// the `M3_AUTHOR_KEY_POLITICAS` kebab-case author-label push
2060 /// every [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
2061 /// coherence gate reads) — two open-coded outer-field accesses
2062 /// that expressed no compile-time link back to the typed slot at
2063 /// the [`Caixa`] altitude. A future extension of the `:politicas`
2064 /// outer axis to a richer author surface (a per-cluster
2065 /// `:politicas-overrides` slot the operator materializes at
2066 /// admission time so a cluster-specific policy can tighten the
2067 /// caixa-declared bound without re-authoring the `caixa.lisp`, a
2068 /// promotion of the plain `Option<MeshPolicy>` to a richer
2069 /// `{static, dynamic}` partition once the M4 per-edge
2070 /// contrato-scoped policy-override surface lands, the M5 traffic-
2071 /// shaping composition the caixa-operator's per-Aplicacao mesh
2072 /// admission webhook keys off) would have had to be threaded
2073 /// through both open-coded copies in lockstep or the Aplicacao-
2074 /// composition seed's default-fold arm would silently disagree
2075 /// with the M3 declared-slot enumerator on which policy composite
2076 /// a given Caixa resolves to — the seed reading an operator-
2077 /// resolved slot while the enumerator's presence probe read the
2078 /// raw slot would silently split the build-time mesh-artifact
2079 /// emission gate from the M3 declared-slot enumerator's kind-
2080 /// coherence gate, a two-consumer split far from the source
2081 /// `caixa.lisp` with no field naming the policy-drift root cause.
2082 /// Lifting the resolution rule to a typed method on the substrate
2083 /// primitive means every downstream consumer of the caixa's per-
2084 /// `Caixa` MESH-COMPOSITION mesh-policy outer-composite surface
2085 /// reaches for exactly one typed dispatch — the resolver's
2086 /// accept-set migrates as a unit on any future axis addition.
2087 ///
2088 /// Third outer top-level [`Caixa`] `Option<&Composite>`-return
2089 /// composite-reference accessor — sibling to the opening
2090 /// [`Self::limits`] (b2bd9d7) and [`Self::behavior`] (35d8b52)
2091 /// accessors on the outer-`Caixa` `Option<&Composite>` composite-
2092 /// reference sub-family, extends the "one typed dispatch on the
2093 /// substrate primitive, thin projections at each consumer"
2094 /// discipline onto the first of the three M3 mesh-slot axes.
2095 /// Peer of the closed inner mesh-slot outer-composite family the
2096 /// sibling [`crate::AplicacaoSpec::politicas`] (534dc21) /
2097 /// [`crate::AplicacaoSpec::placement`] (9abb8f0) /
2098 /// [`crate::AplicacaoSpec::entrada`] (d32111c) composite-reference
2099 /// accessor pins already close on the inner [`crate::AplicacaoSpec`]
2100 /// altitude — opens the outer top-level [`Caixa`] altitude's M3
2101 /// mesh-slot arm of the composite-reference family the remaining
2102 /// two axes (`:placement`, `:entrada`) fold onto in future
2103 /// sibling lifts. Returns `Option<&MeshPolicy>` (not the owning
2104 /// composite by copy or clone) because every downstream consumer
2105 /// of the mesh-policy composite treats it as a read-only per-axis
2106 /// dispatch source — the reference-view is the narrowest borrow
2107 /// that supports every present + roadmapped consumer (per-axis
2108 /// accessor dispatch, `.is_empty()`-gated overlay projection,
2109 /// presence-probe early return on the "author-omitted `:politicas`
2110 /// ⇒ cluster-default applies" partition, `Aplicacao`-composition
2111 /// seed's default-fold arm) without cloning the composite through
2112 /// every consumer's fast path. The `Option` half of the return-
2113 /// type preserves the load-bearing "author-omitted `:politicas` ⇒
2114 /// cluster-default applies" partition (not a default composite
2115 /// the downstream must reject on emptiness) — the accessor
2116 /// projects the raw `Option<MeshPolicy>` slot's presence bit
2117 /// through the reference-return unchanged. Named `politicas()` to
2118 /// match the storage field's name verbatim and the tatara-lisp
2119 /// author-surface term (`:politicas`) the field's own docstring
2120 /// already carries.
2121 #[must_use]
2122 pub fn politicas(&self) -> Option<&crate::aplicacao::MeshPolicy> {
2123 self.politicas.as_ref()
2124 }
2125
2126 /// Substrate-canonical per-`Caixa` `:placement` M3 mesh-slot outer-
2127 /// composite MESH-COMPOSITION-shaped distribution optional-composite-
2128 /// reference accessor every consumer of the top-level manifest's
2129 /// per-Aplicacao [`crate::aplicacao::Placement`] outer-composite
2130 /// reader keys off — returns the author-declared `:placement` typed
2131 /// composite verbatim as an `Option<&Placement>` reference over the
2132 /// same backing storage the raw `self.placement.as_ref()` field
2133 /// access borrows from, with `None` naming the "no `:placement`
2134 /// block authored — every per-axis placement scalar defers to the
2135 /// cluster-default arm named on the per-axis
2136 /// [`crate::aplicacao::Placement::estrategia`] /
2137 /// [`crate::aplicacao::Placement::clusters`] /
2138 /// [`crate::aplicacao::Placement::affinity`] /
2139 /// [`crate::aplicacao::Placement::shard_key`] scalar-accessor
2140 /// docstrings" partition every downstream caixa-mesh /
2141 /// caixa-flux / caixa-helm Aplicacao-artifact emitter treats as
2142 /// "emit no per-`:placement` overlay" and the sibling
2143 /// [`Self::aplicacao_view`] Aplicacao-composition seed folds through
2144 /// the [`crate::aplicacao::Placement::default`] cluster-default arm.
2145 ///
2146 /// The outer `:placement` slot carries the M3 mesh-slot per-
2147 /// Aplicacao typed distribution composite — the load-bearing
2148 /// container of every where-does-this-Aplicacao-run axis every
2149 /// caixa-mesh programs.yaml per-cluster distribution overlay /
2150 /// caixa-flux per-Aplicacao GitRepository/HelmRelease fan-out /
2151 /// future M4 per-Aplicacao Akka-style cluster-sharding entity-id
2152 /// resolver emitter fans on (MESH-COMPOSITION §II.4 — the
2153 /// Aplicacao's typed distribution composite; §V CSE invariants —
2154 /// "distribution is a first-class typed composite, not a runtime
2155 /// scheduler hint" the per-axis scalars enforce; §III.3 — the
2156 /// typed inter-Servico contrato-edge overlay the per-cluster
2157 /// mesh renderer keys off). Every per-`:placement` axis threads
2158 /// through a lifted per-slot accessor on the
2159 /// [`crate::aplicacao::Placement`] type: the
2160 /// [`crate::aplicacao::Placement::estrategia`] (921fe1b)
2161 /// MESH-COMPOSITION distribution-strategy scalar, the
2162 /// [`crate::aplicacao::Placement::clusters`] (a6e18d7) per-cluster
2163 /// distribution-target slice, the [`crate::aplicacao::Placement::affinity`]
2164 /// M3-Adaptive-compression-hint optional-scalar, and the
2165 /// [`crate::aplicacao::Placement::shard_key`] (7cd2a28) Akka-cluster-
2166 /// sharding extractor-expression optional-scalar. Every downstream
2167 /// consumer that reaches for a placement axis first passes through
2168 /// this outer accessor onto the composite and then dispatches onto
2169 /// the per-axis accessor — the two-level dispatch means every per-
2170 /// `:placement` reader now routes through a typed dispatch on the
2171 /// substrate primitive at both altitudes.
2172 ///
2173 /// Composes through [`Self::aplicacao_view`]'s Aplicacao-composition
2174 /// seed: the Aplicacao-view builder folds the outer `Option`'s
2175 /// author-omitted arm onto the [`crate::aplicacao::Placement::default`]
2176 /// cluster-default, so the peer inner [`crate::AplicacaoSpec::placement`]
2177 /// (9abb8f0) `&Placement`-return accessor observes a typed composite
2178 /// whether or not the author declared the outer slot. The outer
2179 /// accessor preserves the "author-omitted vs authored-empty" partition
2180 /// the inner accessor collapses at the cluster-default fold —
2181 /// routing the presence bit through this accessor keeps the
2182 /// [`Self::declared_mesh_slots`] M3 kind-coherence enumerator's
2183 /// `M3_AUTHOR_KEY_PLACEMENT` push separate from the inner
2184 /// [`crate::AplicacaoSpec::validate_placement`]-gated overlay
2185 /// dispatch.
2186 ///
2187 /// Prior to this lift the `.placement` `Option<Placement>`
2188 /// composite was accessed inline at two production sites — the
2189 /// [`Self::aplicacao_view`] Aplicacao-composition seed's
2190 /// `self.placement.clone().unwrap_or_default()` traversal head
2191 /// (caixa-core/src/manifest.rs:2036, which drives the fold onto
2192 /// the [`crate::aplicacao::Placement::default`] cluster-default
2193 /// arm the inner [`crate::AplicacaoSpec::placement`] accessor
2194 /// then observes), and the [`Self::declared_mesh_slots`] M3
2195 /// declared-slot-set enumerator's `self.placement.is_some()`
2196 /// presence probe (caixa-core/src/manifest.rs:2100, which drives
2197 /// the `M3_AUTHOR_KEY_PLACEMENT` kebab-case author-label push
2198 /// every [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
2199 /// coherence gate reads) — two open-coded outer-field accesses
2200 /// that expressed no compile-time link back to the typed slot at
2201 /// the [`Caixa`] altitude. A future extension of the `:placement`
2202 /// outer axis to a richer author surface (a per-cluster
2203 /// `:placement-overrides` slot the operator materializes at
2204 /// admission time so a cluster-specific placement can tighten the
2205 /// caixa-declared bound without re-authoring the `caixa.lisp`, a
2206 /// per-tenant placement-alias table the M4
2207 /// `mesh.pleme.io/v1alpha1/Aplicacao` CR materializer resolves
2208 /// per-CR at admission time, a promotion of the plain
2209 /// `Option<Placement>` to a richer `{static, dynamic}` partition
2210 /// once Orleans-style virtual-actor dynamic placement comes into
2211 /// typed scope) would have had to be threaded through both open-
2212 /// coded copies in lockstep or the Aplicacao-composition seed's
2213 /// default-fold arm would silently disagree with the M3 declared-
2214 /// slot enumerator on which distribution composite a given Caixa
2215 /// resolves to — the seed reading an operator-resolved slot while
2216 /// the enumerator's presence probe read the raw slot would
2217 /// silently split the build-time distribution-artifact emission
2218 /// gate from the M3 declared-slot enumerator's kind-coherence
2219 /// gate, a two-consumer split far from the source `caixa.lisp`
2220 /// with no field naming the distribution-drift root cause.
2221 /// Lifting the resolution rule to a typed method on the substrate
2222 /// primitive means every downstream consumer of the caixa's per-
2223 /// `Caixa` MESH-COMPOSITION distribution outer-composite surface
2224 /// reaches for exactly one typed dispatch — the resolver's
2225 /// accept-set migrates as a unit on any future axis addition.
2226 ///
2227 /// Fourth outer top-level [`Caixa`] `Option<&Composite>`-return
2228 /// composite-reference accessor — sibling to the opening
2229 /// [`Self::limits`] (b2bd9d7) / [`Self::behavior`] (35d8b52) M2-
2230 /// Servico-runtime pair and the peer [`Self::politicas`] (5d23d29)
2231 /// M3-mesh-slot arm on the outer-`Caixa` `Option<&Composite>`
2232 /// composite-reference sub-family, folds on the "one typed
2233 /// dispatch on the substrate primitive, thin projections at each
2234 /// consumer" discipline extended onto the second of the three M3
2235 /// mesh-slot axes. Peer of the closed inner mesh-slot outer-
2236 /// composite family the sibling
2237 /// [`crate::AplicacaoSpec::politicas`] (534dc21) /
2238 /// [`crate::AplicacaoSpec::placement`] (9abb8f0) /
2239 /// [`crate::AplicacaoSpec::entrada`] (d32111c) composite-reference
2240 /// accessor pins already close on the inner
2241 /// [`crate::AplicacaoSpec`] altitude — folds on the outer top-
2242 /// level [`Caixa`] altitude's M3 mesh-slot arm the sibling
2243 /// [`Self::politicas`] opened, extending the discipline onto the
2244 /// second of the three M3 mesh-slot axes. The remaining M3
2245 /// mesh-slot axis (`:entrada`) folds onto this accessor's
2246 /// discipline in the final sibling lift, closing the outer top-
2247 /// level [`Caixa`] `Option<&Composite>` M3 mesh-slot sub-family.
2248 /// Returns `Option<&Placement>` (not the owning composite by copy
2249 /// or clone) because every downstream consumer of the placement
2250 /// composite treats it as a read-only per-axis dispatch source —
2251 /// the reference-view is the narrowest borrow that supports every
2252 /// present + roadmapped consumer (per-axis accessor dispatch,
2253 /// serde composite-serialization on the programs.yaml overlay,
2254 /// presence-probe early return on the "author-omitted `:placement`
2255 /// ⇒ cluster-default applies" partition, `Aplicacao`-composition
2256 /// seed's default-fold arm) without cloning the composite through
2257 /// every consumer's fast path. The `Option` half of the return-
2258 /// type preserves the load-bearing "author-omitted `:placement` ⇒
2259 /// cluster-default applies" partition (not a default composite
2260 /// the downstream must reject on emptiness) — the accessor
2261 /// projects the raw `Option<Placement>` slot's presence bit
2262 /// through the reference-return unchanged. Named `placement()` to
2263 /// match the storage field's name verbatim and the tatara-lisp
2264 /// author-surface term (`:placement`) the field's own docstring
2265 /// already carries.
2266 #[must_use]
2267 pub fn placement(&self) -> Option<&crate::aplicacao::Placement> {
2268 self.placement.as_ref()
2269 }
2270
2271 /// Substrate-canonical per-`Caixa` `:entrada` M3 mesh-slot outer-
2272 /// composite MESH-COMPOSITION-shaped external-gateway optional-
2273 /// composite-reference accessor every consumer of the top-level
2274 /// manifest's per-Aplicacao [`crate::aplicacao::Entrada`] outer-
2275 /// composite reader keys off — returns the author-declared
2276 /// `:entrada` typed composite verbatim as an `Option<&Entrada>`
2277 /// reference over the same backing storage the raw
2278 /// `self.entrada.as_ref()` field access borrows from, with `None`
2279 /// naming the "no `:entrada` block authored — this Aplicacao is
2280 /// cluster-internal, no `Gateway`/`HTTPRoute` fan-out emitted"
2281 /// partition every downstream caixa-mesh Gateway-API artifact
2282 /// emitter treats as "emit no gateway-listener + no `HTTPRoute`
2283 /// backend for this Aplicacao" and the sibling
2284 /// [`Self::aplicacao_view`] Aplicacao-composition seed forwards
2285 /// verbatim (unlike the peer `:politicas` / `:placement` arms,
2286 /// `:entrada` has no cluster-default fold — an omitted `:entrada`
2287 /// stays `None` on the projected [`crate::AplicacaoSpec`] and the
2288 /// peer inner [`crate::AplicacaoSpec::entrada`] accessor observes
2289 /// the same `Option<&Entrada>` presence bit unchanged).
2290 ///
2291 /// The outer `:entrada` slot carries the M3 mesh-slot per-
2292 /// Aplicacao typed external-gateway composite — the load-bearing
2293 /// container of every how-does-the-outside-world-reach-this-
2294 /// Aplicacao axis every caixa-mesh `Gateway`/`HTTPRoute` fan-out
2295 /// emitter fans on (MESH-COMPOSITION §II.5 — the Aplicacao's typed
2296 /// external-entry composite; §V CSE invariants — "the external
2297 /// gateway is a first-class typed composite, not a per-Servico
2298 /// ingress annotation" the per-axis scalars enforce; §III.4 — the
2299 /// typed hostname + backend-Servico pair the per-cluster Gateway-
2300 /// API renderer keys off). Every per-`:entrada` axis threads
2301 /// through a lifted per-slot accessor on the
2302 /// [`crate::aplicacao::Entrada`] type: the
2303 /// [`crate::aplicacao::Entrada::host`] Gateway-API `Listener.hostname`
2304 /// scalar, the [`crate::aplicacao::Entrada::para`] backend-Servico
2305 /// caixa-name scalar, the [`crate::aplicacao::Entrada::paths`]
2306 /// per-rule `HTTPPathMatch` list, the [`crate::aplicacao::Entrada::port`]
2307 /// backend `trigger.service.port` scalar, and the
2308 /// [`crate::aplicacao::Entrada::resolved_paths`] URL-path fallback
2309 /// resolver every HTTPRoute-aware renderer consumes. Every
2310 /// downstream consumer that reaches for an entry axis first passes
2311 /// through this outer accessor onto the composite and then
2312 /// dispatches onto the per-axis accessor — the two-level dispatch
2313 /// means every per-`:entrada` reader now routes through a typed
2314 /// dispatch on the substrate primitive at both altitudes.
2315 ///
2316 /// Composes through [`Self::aplicacao_view`]'s Aplicacao-composition
2317 /// seed: the Aplicacao-view builder forwards the outer `Option`
2318 /// arm verbatim (no default fold — `:entrada` is inherently
2319 /// optional; a cluster-internal Aplicacao has no external gateway
2320 /// at all, not "an external gateway that defaults to nothing"), so
2321 /// the peer inner [`crate::AplicacaoSpec::entrada`] (d32111c)
2322 /// `Option<&Entrada>`-return accessor observes the same presence
2323 /// bit whether or not the author declared the outer slot. Routing
2324 /// the presence bit through this accessor keeps the
2325 /// [`Self::declared_mesh_slots`] M3 kind-coherence enumerator's
2326 /// `M3_AUTHOR_KEY_ENTRADA` push separate from the inner
2327 /// [`crate::AplicacaoSpec::validate_entrada`]-gated
2328 /// hostname/backend/path emission dispatch.
2329 ///
2330 /// Prior to this lift the `.entrada` `Option<Entrada>` composite
2331 /// was accessed inline at two production sites — the
2332 /// [`Self::aplicacao_view`] Aplicacao-composition seed's
2333 /// `self.entrada.clone()` traversal head (caixa-core/src/manifest.rs:2182,
2334 /// which drives the forward onto the peer inner
2335 /// [`crate::AplicacaoSpec::entrada`] accessor the caixa-mesh
2336 /// Gateway-API fan-out then observes), and the
2337 /// [`Self::declared_mesh_slots`] M3 declared-slot-set enumerator's
2338 /// `self.entrada.is_some()` presence probe (caixa-core/src/manifest.rs:2248,
2339 /// which drives the `M3_AUTHOR_KEY_ENTRADA` kebab-case author-
2340 /// label push every [`crate::LayoutError::MeshSlotsOnNonAplicacao`]
2341 /// kind-coherence gate reads) — two open-coded outer-field
2342 /// accesses that expressed no compile-time link back to the typed
2343 /// slot at the [`Caixa`] altitude. A future extension of the
2344 /// `:entrada` outer axis to a richer author surface (a per-cluster
2345 /// `:entrada-overrides` slot the operator materializes at admission
2346 /// time so a cluster-specific hostname can pin the caixa-declared
2347 /// bound without re-authoring the `caixa.lisp`, a per-tenant
2348 /// gateway-alias table the M4 `mesh.pleme.io/v1alpha1/Aplicacao`
2349 /// CR materializer resolves per-CR at admission time, a promotion
2350 /// of the plain `Option<Entrada>` to a richer
2351 /// `{public, private, internal}` partition once Cilium-identity-
2352 /// scoped internal gateways come into typed scope) would have had
2353 /// to be threaded through both open-coded copies in lockstep or the
2354 /// Aplicacao-composition seed's forward arm would silently
2355 /// disagree with the M3 declared-slot enumerator on which external-
2356 /// gateway composite a given Caixa resolves to — the seed reading
2357 /// an operator-resolved slot while the enumerator's presence probe
2358 /// read the raw slot would silently split the build-time gateway-
2359 /// artifact emission gate from the M3 declared-slot enumerator's
2360 /// kind-coherence gate, a two-consumer split far from the source
2361 /// `caixa.lisp` with no field naming the entry-drift root cause.
2362 /// Lifting the resolution rule to a typed method on the substrate
2363 /// primitive means every downstream consumer of the caixa's per-
2364 /// `Caixa` MESH-COMPOSITION external-gateway outer-composite
2365 /// surface reaches for exactly one typed dispatch — the resolver's
2366 /// accept-set migrates as a unit on any future axis addition.
2367 ///
2368 /// Fifth and final outer top-level [`Caixa`] `Option<&Composite>`-
2369 /// return composite-reference accessor — closes the outer-`Caixa`
2370 /// `Option<&Composite>` composite-reference sub-family opened by
2371 /// [`Self::limits`] (b2bd9d7) / [`Self::behavior`] (35d8b52) on the
2372 /// M2 Servico-runtime arm and extended onto the M3 mesh-slot arm
2373 /// by [`Self::politicas`] (5d23d29) / [`Self::placement`] (4fb8074),
2374 /// folds on the "one typed dispatch on the substrate primitive,
2375 /// thin projections at each consumer" discipline extended onto the
2376 /// third and final M3 mesh-slot axis. Peer of the closed inner
2377 /// mesh-slot outer-composite family the sibling
2378 /// [`crate::AplicacaoSpec::politicas`] (534dc21) /
2379 /// [`crate::AplicacaoSpec::placement`] (9abb8f0) /
2380 /// [`crate::AplicacaoSpec::entrada`] (d32111c) composite-reference
2381 /// accessor pins already close on the inner
2382 /// [`crate::AplicacaoSpec`] altitude — this lift closes the mirror
2383 /// sub-family on the outer top-level [`Caixa`] altitude, so both
2384 /// altitudes of the outer-composite reference-return discipline
2385 /// (per-`Caixa` outer-slot presence + per-`AplicacaoSpec` inner-
2386 /// slot presence) now carry the full five-arm accept-set behind a
2387 /// typed dispatch on the substrate primitive. Returns
2388 /// `Option<&Entrada>` (not the owning composite by copy or clone)
2389 /// because every downstream consumer of the entrada composite
2390 /// treats it as a read-only per-axis dispatch source — the
2391 /// reference-view is the narrowest borrow that supports every
2392 /// present + roadmapped consumer (per-axis accessor dispatch,
2393 /// serde composite-serialization on the programs.yaml overlay,
2394 /// presence-probe early return on the "author-omitted `:entrada`
2395 /// ⇒ cluster-internal Aplicacao" partition, `Aplicacao`-composition
2396 /// seed's forward arm) without cloning the composite through every
2397 /// consumer's fast path. The `Option` half of the return-type
2398 /// preserves the load-bearing "author-omitted `:entrada` ⇒
2399 /// cluster-internal Aplicacao" partition (not a default composite
2400 /// the downstream must reject on emptiness — a cluster-internal
2401 /// Aplicacao has no external gateway at all, not "a default gateway
2402 /// that emits nothing"); the accessor projects the raw
2403 /// `Option<Entrada>` slot's presence bit through the reference-
2404 /// return unchanged. Named `entrada()` to match the storage field's
2405 /// name verbatim and the tatara-lisp author-surface term
2406 /// (`:entrada`) the field's own docstring already carries.
2407 #[must_use]
2408 pub fn entrada(&self) -> Option<&crate::aplicacao::Entrada> {
2409 self.entrada.as_ref()
2410 }
2411
2412 /// Substrate-canonical per-`Caixa` `:ci` slot accessor — returns the
2413 /// author-declared typed CI run (`canteiro_types::CiRun`) verbatim as
2414 /// an `Option<&CiRun>`, borrowed from the typed slot's own
2415 /// `Option<CiRun>` storage. `None` when the slot is absent (every
2416 /// non-`Acao` kind, and an `Acao` caixa that hasn't declared `:ci`
2417 /// yet — the latter is caught by [`crate::LayoutError::MissingCi`],
2418 /// not silently accepted).
2419 ///
2420 /// Named `ci()` to match the storage field's name and the
2421 /// tatara-lisp author surface (`:ci`); mirrors the sibling
2422 /// `Option<&Composite>` accessors on this same `Caixa` altitude
2423 /// ([`Self::limits`], [`Self::behavior`], [`Self::politicas`],
2424 /// [`Self::placement`], [`Self::entrada`]) — one typed dispatch on
2425 /// the substrate primitive rather than an open-coded `self.ci.as_ref()`
2426 /// at every consumer.
2427 #[must_use]
2428 pub fn ci(&self) -> Option<&canteiro_types::CiRun> {
2429 self.ci.as_ref()
2430 }
2431
2432 /// Substrate-canonical per-`Caixa` `:estrategia` M2 supervisor-tree-
2433 /// slot flat-spread OTP-shaped sibling-restart-strategy discriminant
2434 /// accessor every consumer of the top-level manifest's per-Supervisor
2435 /// restart-strategy axis keys off — returns the author-declared
2436 /// `:estrategia` variant verbatim as an `Option<RestartStrategy>`,
2437 /// `Copy`-projected from the typed slot's own
2438 /// `Option<crate::supervisor::RestartStrategy>` storage. Optional
2439 /// (`:estrategia` is a flat-spread supervisor-only slot every
2440 /// non-`Supervisor`-kind `defcaixa` carries as `None` by
2441 /// `#[serde(default)]`, and every `Supervisor`-kind `defcaixa` may
2442 /// still omit to defer to [`RestartStrategy::default`] —
2443 /// [`RestartStrategy::OneForOne`] — through the [`Self::supervisor_view`]
2444 /// `unwrap_or_default()` fold; a returned `None` degenerates to the
2445 /// [`SupervisorSpec::default`]-inherited strategy without any silent
2446 /// promotion to a fresh explicit variant at the accessor boundary).
2447 ///
2448 /// The `:estrategia` slot carries the M2 typed OTP-shaped sibling-
2449 /// restart-strategy discriminant every substrate-side per-Supervisor
2450 /// dispatch fans on (INSPIRATIONS §II.2 — OTP `supervisor:strategy`
2451 /// closed-set `one_for_one | one_for_all | rest_for_one |
2452 /// simple_one_for_one` algebra translated onto pleme-io's typed
2453 /// [`RestartStrategy`] enum; CAIXA-SDLC §II — the M2 supervisor-tree
2454 /// slot algebra the operator's hierarchical reconciliation scheduler
2455 /// fans on). The slot is *flat-spread* on the outer top-level `Caixa`
2456 /// (per the field-shape docstring at caixa-core/src/manifest.rs — "The
2457 /// supervisor slots are flat on Caixa (vs nested under a
2458 /// `SupervisorSpec` sub-form) to keep tatara-lisp authoring at one
2459 /// level of nesting"), so the accessor's altitude is the outer
2460 /// [`Caixa`] surface rather than the composed [`SupervisorSpec`]
2461 /// altitude the sibling [`crate::supervisor::SupervisorSpec::estrategia`]
2462 /// (eafb619) accessor keys off. The two typed axes — the outer
2463 /// author-surface `Option<RestartStrategy>` on the [`Caixa`] altitude
2464 /// (author-omitted arm carried as `None`) and the inner post-
2465 /// composition `RestartStrategy` on the [`SupervisorSpec`] altitude
2466 /// (`Option` collapsed through the [`Self::supervisor_view`]
2467 /// `unwrap_or_default()` fold) — now share one accessor discipline for
2468 /// the shared substrate concept "the author-declared OTP-shaped
2469 /// sibling-restart-strategy variant that partitions the downstream
2470 /// per-Supervisor renderer's per-arm fan-out"; the outer-altitude
2471 /// `None` arm is the pre-composition presence bit every declared-slot
2472 /// enumerator ([`Self::declared_supervisor_slots`]) reads, and the
2473 /// inner-altitude non-`Option` `RestartStrategy` is the post-
2474 /// composition partition-dispatch input every strategy-arm consumer
2475 /// ([`SupervisorSpec::validate`], the future wasm-operator's per-
2476 /// Supervisor sibling-restart branch, the future M4
2477 /// `mesh.pleme.io/v1alpha1/Supervisor` CR materializer's admission
2478 /// webhook) fans on.
2479 ///
2480 /// Prior to this lift the `.estrategia` field was accessed inline at
2481 /// two production sites in `caixa-core/src/manifest.rs` — the
2482 /// [`Self::declared_supervisor_slots`] `SUPERVISOR_AUTHOR_KEY_ESTRATEGIA`
2483 /// presence-probe arm at `if self.estrategia.is_some()` (which drives
2484 /// the [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] kind-
2485 /// coherence gate's per-slot label push) and the [`Self::supervisor_view`]
2486 /// `SupervisorSpec` construction site at `estrategia:
2487 /// self.estrategia.unwrap_or_default()` (which composes the flat-
2488 /// spread outer author-surface `Option<RestartStrategy>` onto the
2489 /// inner post-composition [`SupervisorSpec`] `RestartStrategy` field
2490 /// the [`SupervisorSpec::estrategia`] accessor keys off) — two open-
2491 /// coded field-accesses that expressed no compile-time link back to
2492 /// the typed slot. A future extension of the outer `:estrategia` axis
2493 /// to a richer author surface (a per-cluster strategy override the
2494 /// operator pins through a future `:estrategia-overrides` overlay the
2495 /// MESH-COMPOSITION §III.2 supervision-canary roadmap acknowledges,
2496 /// a per-tenant strategy-alias table the M4 CR materializer resolves
2497 /// per-CR, a per-Supervisor dynamic strategy derivation the future
2498 /// adaptive-supervision engine computes from child-failure-history
2499 /// topology, a per-child-cohort strategy split the future
2500 /// `RestForCohort` extension the INSPIRATIONS.md §II.2 Erlang/OTP
2501 /// absorption roadmap acknowledges, a promotion of the plain
2502 /// `Option<RestartStrategy>` to a richer
2503 /// `AuthorDeclaredStrategy { declared, overlay }` newtype once the
2504 /// operator-resolved overlay lands) would have had to be threaded
2505 /// through both open-coded copies in lockstep or the enumerator's
2506 /// presence probe and the composition site's `unwrap_or_default()`
2507 /// fold would silently disagree on which strategy a given [`Caixa`]
2508 /// resolves to (an author's `:estrategia OneForAll` would satisfy
2509 /// the enumerator's presence probe while the composition site
2510 /// silently rendered a stale `OneForOne`, or vice versa). Lifting
2511 /// the resolution rule to a typed method on the substrate primitive
2512 /// means every downstream consumer of the caixa's per-`Caixa` outer-
2513 /// altitude sibling-restart-strategy surface reaches for exactly one
2514 /// typed dispatch — the resolver's accept-set migrates as a unit on
2515 /// any future axis addition.
2516 ///
2517 /// First outer top-level [`Caixa`] `Option<Copy>`-return supervisor-
2518 /// tree-slot flat-spread accessor for M2 supervisor-slot Copy-carry
2519 /// axes — opens the outer-`Caixa` `Option<Copy>` flat-spread
2520 /// projection pattern the sibling per-`Caixa` `:max-restarts`
2521 /// `Option<u32>` and (through the future duration-newtype landing)
2522 /// `:restart-window` `Option<Duration>` future outer-scalar lifts
2523 /// fold on. Peer of the inner-altitude [`crate::supervisor::SupervisorSpec::estrategia`]
2524 /// (eafb619) `Copy`-return sibling-restart-strategy scalar accessor on
2525 /// the post-composition [`SupervisorSpec`] altitude — same "one
2526 /// typed dispatch on the substrate primitive, thin projections at
2527 /// each consumer" discipline extended onto the pre-composition outer
2528 /// author-surface [`Caixa`] altitude for the same OTP-shaped
2529 /// sibling-restart-strategy axis. Peer of the closed outer-`Caixa`
2530 /// `Option<&Composite>` composite-reference family the sibling
2531 /// [`Self::limits`] (b2bd9d7) / [`Self::behavior`] (35d8b52) /
2532 /// [`Self::politicas`] (5d23d29) / [`Self::placement`] (4fb8074) /
2533 /// [`Self::entrada`] (e4128e4) accessor pins already carry on the
2534 /// outer `Option<&Composite>` altitude — extends the outer-`Caixa`
2535 /// typed-slot accessor discipline onto the flat-spread M2 supervisor-
2536 /// tree `Option<Copy>`-discriminant sub-family the sibling M3
2537 /// [`crate::aplicacao::Placement::estrategia`] (921fe1b)
2538 /// `PlacementStrategy` `Copy`-composite-enum scalar accessor already
2539 /// pins on the inner-altitude per-`:placement` composite. Named
2540 /// `estrategia()` to match the storage field's name and the
2541 /// per-[`SupervisorSpec`] peer [`crate::supervisor::SupervisorSpec::estrategia`]
2542 /// / per-[`crate::aplicacao::Placement`] peer
2543 /// [`crate::aplicacao::Placement::estrategia`] method-name discipline
2544 /// verbatim; the accessor's identity name maps onto the canonical
2545 /// OTP-shape supervision vocabulary the [`RestartStrategy`] enum's
2546 /// docstring already carries.
2547 #[must_use]
2548 pub fn estrategia(&self) -> Option<crate::supervisor::RestartStrategy> {
2549 self.estrategia
2550 }
2551
2552 /// Substrate-canonical per-`Caixa` `:max-restarts` M2 supervisor-tree-
2553 /// slot flat-spread OTP-`MaxIntensity`-shaped restart-budget-count
2554 /// scalar accessor every consumer of the top-level manifest's per-
2555 /// Supervisor `:max-restarts` restart-budget-count axis keys off —
2556 /// returns the author-declared `:max-restarts` typed `Option<u32>`
2557 /// verbatim, `Copy`-projected from the typed slot's own `Option<u32>`
2558 /// storage (`u32` is `Copy`, so `Option<u32>` is `Copy` and the
2559 /// accessor returns by value; no borrow of `&self` past the call).
2560 /// Optional (`:max-restarts` is a flat-spread supervisor-only slot
2561 /// every non-`Supervisor`-kind `defcaixa` carries as `None` by
2562 /// `#[serde(default)]`, and every `Supervisor`-kind `defcaixa` may
2563 /// still omit to defer to the [`Self::supervisor_view`]
2564 /// `unwrap_or(5)` fold's OTP-canonical `{intensity, 5, 60}` default).
2565 ///
2566 /// The `:max-restarts` slot carries the M2 typed Erlang/OTP-shaped
2567 /// `MaxIntensity` restart-budget count that pairs with the sibling
2568 /// `:restart-window` `Period` to form the `MaxIntensity / Period`
2569 /// restart-intensity ratio the supervisor trips its own escalation on
2570 /// (INSPIRATIONS §II.2 — Erlang/OTP `supervisor` `{intensity, 5, 60}`
2571 /// worker-supervisor default; RUNTIME-PATTERNS §II.2; CAIXA-SDLC §II
2572 /// — the M2 supervisor-tree slot algebra the operator's hierarchical
2573 /// reconciliation scheduler fans on). The slot is *flat-spread* on
2574 /// the outer top-level `Caixa` (per the field-shape docstring at
2575 /// caixa-core/src/manifest.rs — "The supervisor slots are flat on
2576 /// Caixa (vs nested under a `SupervisorSpec` sub-form)"), so the
2577 /// accessor's altitude is the outer [`Caixa`] surface rather than the
2578 /// composed [`SupervisorSpec`] altitude the sibling
2579 /// [`crate::supervisor::SupervisorSpec::max_restarts`] accessor keys
2580 /// off. The two typed axes — the outer author-surface `Option<u32>`
2581 /// on the [`Caixa`] altitude (author-omitted arm carried as `None`)
2582 /// and the inner post-composition `u32` on the [`SupervisorSpec`]
2583 /// altitude (`Option` collapsed through the [`Self::supervisor_view`]
2584 /// `unwrap_or(5)` fold) — now share one accessor discipline for the
2585 /// shared substrate concept "the author-declared OTP-shaped
2586 /// restart-budget count every downstream per-Supervisor consumer's
2587 /// restart-intensity budget-vs-count comparator fans on".
2588 ///
2589 /// Prior to this lift the `.max_restarts` field was accessed inline
2590 /// at two production sites in `caixa-core/src/manifest.rs` — the
2591 /// [`Self::declared_supervisor_slots`] `SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS`
2592 /// presence-probe arm at `if self.max_restarts.is_some()` (which
2593 /// drives the [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
2594 /// kind-coherence gate's per-slot label push) and the
2595 /// [`Self::supervisor_view`] `SupervisorSpec` construction site at
2596 /// `max_restarts: self.max_restarts.unwrap_or(5)` (which composes the
2597 /// flat-spread outer author-surface `Option<u32>` onto the inner
2598 /// post-composition [`SupervisorSpec`] `u32` field the
2599 /// [`SupervisorSpec::max_restarts`] accessor keys off) — two open-
2600 /// coded field-accesses that expressed no compile-time link back to
2601 /// the typed slot. A future extension of the outer `:max-restarts`
2602 /// axis to a richer author surface (a per-cluster restart-budget
2603 /// override the operator pins through a future `:max-restarts-overrides`
2604 /// overlay the MESH-COMPOSITION §III.2 supervision-canary roadmap
2605 /// acknowledges, a per-tenant restart-budget-alias table the M4 CR
2606 /// materializer resolves per-CR, a per-Supervisor dynamic restart-
2607 /// budget derivation the future adaptive-supervision engine computes
2608 /// from child-failure-history topology, a promotion of the plain
2609 /// `Option<u32>` count to a richer `{MaxR, MaxT}` per-child-cohort
2610 /// restart-budget-partition once the INSPIRATIONS §II.2 Erlang/OTP
2611 /// per-child-cohort roadmap lands) would have had to be threaded
2612 /// through both open-coded copies in lockstep or the enumerator's
2613 /// presence probe and the composition site's `unwrap_or(5)` fold
2614 /// would silently disagree on which restart-budget a given [`Caixa`]
2615 /// resolves to (an author's `:max-restarts 10` would satisfy the
2616 /// enumerator's presence probe while the composition site silently
2617 /// composed the OTP-canonical `5`, or vice versa). Lifting the
2618 /// resolution rule to a typed method on the substrate primitive means
2619 /// every downstream consumer of the caixa's per-`Caixa` outer-altitude
2620 /// restart-budget-count surface reaches for exactly one typed dispatch
2621 /// — the resolver's accept-set migrates as a unit on any future axis
2622 /// addition.
2623 ///
2624 /// Second outer top-level [`Caixa`] `Option<Copy>`-return supervisor-
2625 /// tree-slot flat-spread accessor for M2 supervisor-slot Copy-carry
2626 /// axes — folds on the outer-`Caixa` `Option<Copy>` flat-spread
2627 /// projection pattern the sibling per-`Caixa`
2628 /// [`Self::estrategia`] (ed04d3c) accessor opened, extends the
2629 /// sub-family onto the sibling `Option<u32>` restart-budget-count arm.
2630 /// Peer of the inner-altitude
2631 /// [`crate::supervisor::SupervisorSpec::max_restarts`] `u32` accessor
2632 /// on the post-composition [`SupervisorSpec`] altitude — same "one
2633 /// typed dispatch on the substrate primitive, thin projections at
2634 /// each consumer" discipline extended onto the pre-composition outer
2635 /// author-surface [`Caixa`] altitude for the same OTP-`MaxIntensity`-
2636 /// shaped restart-budget-count axis. Named `max_restarts()` to match
2637 /// the storage field's name and the per-[`SupervisorSpec`] peer
2638 /// [`crate::supervisor::SupervisorSpec::max_restarts`] method-name
2639 /// discipline verbatim; the accessor's identity maps onto the
2640 /// canonical OTP-shape supervision vocabulary the `:max-restarts`
2641 /// field's docstring already carries.
2642 #[must_use]
2643 pub const fn max_restarts(&self) -> Option<u32> {
2644 self.max_restarts
2645 }
2646
2647 /// Substrate-canonical per-`Caixa` `:restart-window` M2 supervisor-
2648 /// tree-slot flat-spread OTP-`Period`-shaped restart-intensity-
2649 /// denominator raw-duration-string scalar accessor every consumer of
2650 /// the top-level manifest's per-Supervisor `:restart-window` sliding-
2651 /// window axis keys off — returns the author-declared `:restart-window`
2652 /// typed `Option<String>` verbatim as an `Option<&str>`, borrowed
2653 /// from the typed slot's own `Option<String>` storage. `None` when
2654 /// the slot is absent (the canonical "never reset — every restart
2655 /// across the supervisor's lifetime counts against the sibling
2656 /// `:max-restarts` budget" sentinel every non-`Supervisor`-kind
2657 /// `defcaixa` carries by `#[serde(default)]` and every
2658 /// `Supervisor`-kind `defcaixa` may still omit to defer to the
2659 /// [`Self::supervisor_view`] `restart_window: None` composition
2660 /// through the [`crate::supervisor::duration_codec::parse`] soft-
2661 /// swallow `.and_then(|s| … .ok())` fold).
2662 ///
2663 /// The `:restart-window` slot carries the raw M2 typed Erlang/OTP-
2664 /// shaped `Period` sliding-observation-interval duration string that
2665 /// pairs with the sibling `:max-restarts` `MaxIntensity` restart-
2666 /// budget count to form the `MaxIntensity / Period` restart-intensity
2667 /// ratio the supervisor trips its own escalation on (INSPIRATIONS
2668 /// §II.2 — Erlang/OTP `supervisor` `{intensity, 5, 60}` worker-
2669 /// supervisor default; RUNTIME-PATTERNS §II.2). The outer-`Caixa`
2670 /// slot stores the raw duration string (`"60s"`, `"5m"`, `"500ms"`)
2671 /// authored under `:restart-window` — the typed [`SupervisorSpec`]
2672 /// holds an `Option<Duration>` routed through the shared
2673 /// [`crate::supervisor::duration_codec`] via `with = "duration_codec"`
2674 /// — so the outer altitude's accessor returns `Option<&str>` (raw
2675 /// authoring surface) while the inner altitude's
2676 /// [`crate::supervisor::SupervisorSpec::restart_window`] returns
2677 /// `Option<Duration>` (parsed typed surface). The parse-refusal arm
2678 /// is closed by the sibling [`Self::validate_restart_window`] gate
2679 /// that surfaces [`ManifestError::RestartWindowMalformed`] naming
2680 /// the offending value; the view-construction path
2681 /// [`Self::supervisor_view`] soft-swallows the same parse error to
2682 /// `None` to keep the view best-effort.
2683 ///
2684 /// Prior to this lift the `.restart_window` field was accessed inline
2685 /// at three production sites in `caixa-core/src/manifest.rs` — the
2686 /// [`Self::declared_supervisor_slots`]
2687 /// `SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW` presence-probe arm at
2688 /// `if self.restart_window.is_some()` (which drives the
2689 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] kind-
2690 /// coherence gate's per-slot label push), the
2691 /// [`Self::validate_restart_window`] `let Some(s) =
2692 /// self.restart_window.as_deref()` empty-and-shape gate binding
2693 /// (which folds the raw string through the shared
2694 /// [`crate::supervisor::duration_codec::parse`] to surface
2695 /// [`ManifestError::RestartWindowMalformed`] naming the offending
2696 /// value), and the [`Self::supervisor_view`] `self.restart_window
2697 /// .as_deref().and_then(…)` view-construction fold (which composes
2698 /// the flat-spread outer author-surface `Option<String>` onto the
2699 /// inner post-composition [`SupervisorSpec`] `Option<Duration>`
2700 /// field the [`SupervisorSpec::restart_window`] accessor keys off) —
2701 /// three open-coded field-accesses that expressed no compile-time
2702 /// link back to the typed slot. A future extension of the outer
2703 /// `:restart-window` axis to a richer author surface (a per-cluster
2704 /// window override, a per-tenant window-alias table, a per-Supervisor
2705 /// dynamic window derivation the future adaptive-supervision engine
2706 /// computes from child-failure-history topology, a promotion of the
2707 /// plain `Option<String>` raw duration to a typed `Option<Duration>`
2708 /// once the future author-surface parser lands at the [`Caixa`]
2709 /// altitude and the raw-string form is retired) would have had to be
2710 /// threaded through every open-coded copy in lockstep or the three
2711 /// consumers would silently disagree on which raw string a given
2712 /// [`Caixa`] resolves to. Lifting the resolution rule to a typed
2713 /// method on the substrate primitive means every downstream consumer
2714 /// of the caixa's per-`Caixa` outer-altitude restart-window raw-
2715 /// string surface reaches for exactly one typed dispatch — the
2716 /// resolver's accept-set migrates as a unit on any future axis
2717 /// addition.
2718 ///
2719 /// Third outer top-level [`Caixa`] supervisor-tree-slot flat-spread
2720 /// accessor — folds on the outer-`Caixa` M2 supervisor-tree flat-
2721 /// spread projection pattern the sibling per-`Caixa`
2722 /// [`Self::estrategia`] (ed04d3c) `Option<Copy>` and
2723 /// [`Self::max_restarts`] `Option<Copy>` accessors opened, extends
2724 /// the sub-family onto the sibling `Option<&str>` raw-duration-
2725 /// string arm (the outer altitude's raw-string form; the inner
2726 /// altitude's parsed [`Duration`] form is the peer
2727 /// [`crate::supervisor::SupervisorSpec::restart_window`] accessor).
2728 /// Peer of the sibling per-`Caixa` `Option<&str>`-return scalar
2729 /// accessors ([`Self::licenca`] / [`Self::repositorio`] /
2730 /// [`Self::descricao`] / [`Self::edicao`]) on the universal-axis
2731 /// outer scalar-projection family the outer-`Caixa` `Option<&str>`
2732 /// sub-family already carries — same "one typed dispatch on the
2733 /// substrate primitive, thin projections at each consumer"
2734 /// discipline extended onto the M2 supervisor-tree flat-spread
2735 /// `Option<&str>` raw-duration-string arm. Named `restart_window()`
2736 /// to match the storage field's name and the per-[`SupervisorSpec`]
2737 /// peer [`crate::supervisor::SupervisorSpec::restart_window`]
2738 /// method-name discipline verbatim; the accessor's identity maps
2739 /// onto the canonical OTP-shape supervision vocabulary the
2740 /// `:restart-window` field's docstring already carries.
2741 #[must_use]
2742 pub fn restart_window(&self) -> Option<&str> {
2743 self.restart_window.as_deref()
2744 }
2745
2746 /// Substrate-canonical per-`Caixa` `:upgrade-from` M2 typed-slot
2747 /// outer-composite OTP-appup-shaped per-prior-version migration-
2748 /// entry-list slice accessor every consumer of the top-level
2749 /// manifest's per-Servico hot-upgrade-block `&[UpgradeFromEntry]`
2750 /// slice-view keys off — returns the author-declared `:upgrade-from`
2751 /// typed `Vec<UpgradeFromEntry>` verbatim as a
2752 /// `&[UpgradeFromEntry]` slice-view over the same backing buffer
2753 /// the raw `self.upgrade_from.as_slice()` field access borrows
2754 /// from. Empty-slice-carrying (the "no hot-upgrade path declared"
2755 /// arm every `defcaixa` without an `:upgrade-from` block carries;
2756 /// the [`Self::from_lisp`] derive folds an omitted `:upgrade-from`
2757 /// through `#[serde(default)]` to `Vec::new()`, so a `Caixa` past
2758 /// parse definitionally carries a `Vec<UpgradeFromEntry>` slot —
2759 /// possibly empty — and the returned `&[UpgradeFromEntry]`
2760 /// degenerates to an empty slice on that arm without any silent
2761 /// `None` collapse).
2762 ///
2763 /// The outer `:upgrade-from` slot carries the M2 typed OTP-appup
2764 /// migration block — the load-bearing container of every per-
2765 /// prior-`:versao` migration-instruction list the wasm-operator
2766 /// dispatches on at hot-upgrade time (INSPIRATIONS §II.4 — OTP
2767 /// `.appup` per-prior-version `LoadModule | StateChange |
2768 /// SoftPurge | Purge | Restart` instruction algebra translated
2769 /// onto pleme-io's typed `:upgrade-from :from` + `:instructions`
2770 /// entry list; CAIXA-SDLC §II — the typed-M2 slot algebra the
2771 /// operator's hot-upgrade dispatch fans on). Every per-entry axis
2772 /// threads through a lifted per-entry accessor on the
2773 /// [`UpgradeFromEntry`] type: the
2774 /// [`UpgradeFromEntry::prior_versao`] SemVer-shaped previous-
2775 /// version scalar accessor and the
2776 /// [`UpgradeFromEntry::instructions`] `&[UpgradeInstruction]`-
2777 /// return per-entry instruction-list accessor (0137e5a). Every
2778 /// downstream consumer of the hot-upgrade path first passes
2779 /// through this outer accessor onto the slice and then dispatches
2780 /// per-entry through the inner accessors — the two-level dispatch
2781 /// means every per-`:upgrade-from` reader now routes through a
2782 /// typed dispatch on the substrate primitive at both altitudes.
2783 ///
2784 /// Prior to this lift the `.upgrade_from` `Vec<UpgradeFromEntry>`
2785 /// slot was accessed inline at production sites across three
2786 /// files — the [`Self::declared_servico_slots`] M2 declared-slot
2787 /// enumerator's `self.upgrade_from.is_empty()` presence probe
2788 /// (caixa-core/src/manifest.rs, which drives the
2789 /// `M2_AUTHOR_KEY_UPGRADE_FROM` kebab-case author-label push every
2790 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] kind-coherence
2791 /// gate reads), the [`crate::StandardLayout::verify`] per-
2792 /// `:upgrade-from` three-stage validation pass (caixa-core/src/
2793 /// layout.rs, which fans onto the
2794 /// [`crate::upgrade::validate_upgrade_from`] per-entry shape +
2795 /// cross-entry duplicate gate, the
2796 /// [`crate::upgrade::validate_upgrade_from_against_versao`]
2797 /// SemVer-precedence cross-slot gate, the
2798 /// [`crate::upgrade::validate_upgrade_from_against_behavior`]
2799 /// `:state-change` ↔ `:on-state-change` cross-slot composition
2800 /// gate, and the per-instruction script-path existence-probe walk
2801 /// that reads each entry's [`UpgradeFromEntry::instructions`] to
2802 /// resolve every declared migration script against the layout
2803 /// root), and the [`crate::render::servico_m2_overlay`] per-
2804 /// Servico M2 overlay emitter's `!caixa.upgrade_from.is_empty()`
2805 /// presence gate + `serde_yaml::to_value(&caixa.upgrade_from)`
2806 /// projection (caixa-core/src/render.rs, which drives the
2807 /// `M2_KEY_UPGRADE_FROM`-keyed `serde_yaml` projection every
2808 /// `caixa-helm` / `caixa-flux` Servico values-block emitter fans
2809 /// on and lands as the ComputeUnit CR's `spec.upgradeFrom` field).
2810 /// A future extension of the outer `:upgrade-from` axis (a per-
2811 /// cluster `:upgrade-overrides` overlay the wasm-engine operator
2812 /// resolves at admission time so a cluster-specific migration
2813 /// policy can tighten a caixa-declared step without re-authoring
2814 /// the `caixa.lisp`, promotion of the plain
2815 /// `Vec<UpgradeFromEntry>` to a richer `{static, dynamic}`
2816 /// partition once runtime-resolved hot-upgrade instructions land,
2817 /// per-entry priority annotation once multi-strategy fan-out
2818 /// lands) would have had to be threaded through all six open-
2819 /// coded copies in lockstep or one consumer would silently
2820 /// disagree with the peers on which upgrade slice a given Caixa
2821 /// resolves to — a six-consumer split at the enumerator, the
2822 /// three-stage validate pass, the script-path probe walk, and the
2823 /// M2 overlay emitter, far from the source `caixa.lisp` with no
2824 /// field naming the upgrade-drift root cause. Lifting the
2825 /// resolution rule to a typed method on the substrate primitive
2826 /// means every downstream consumer of the caixa's per-`Caixa`
2827 /// OTP-appup outer-slice surface reaches for exactly one typed
2828 /// dispatch — the resolver's accept-set migrates as a unit on any
2829 /// future axis addition.
2830 ///
2831 /// First outer top-level [`Caixa`] `&[Composite]`-return slice
2832 /// accessor for M2 / M3 typed-slot vec-carry axes — opens the
2833 /// outer-`Caixa` `&[Composite]` composite-slice projection
2834 /// pattern the sibling `:children`
2835 /// [`crate::supervisor::ChildSpec`] / `:membros`
2836 /// [`crate::aplicacao::Membro`] / `:contratos`
2837 /// [`crate::aplicacao::WitContract`] future outer-composite-slice
2838 /// lifts fold on. Peer of the closed outer-`Caixa` scalar
2839 /// `Option<&Composite>` composite-reference family the sibling
2840 /// [`Self::limits`] (b2bd9d7) / [`Self::behavior`] (35d8b52) /
2841 /// [`Self::politicas`] (5d23d29) / [`Self::placement`] (4fb8074) /
2842 /// [`Self::entrada`] (e4128e4) accessors closed on the outer
2843 /// `Option<&Composite>` altitude, extended here to the outer-
2844 /// `Caixa` `&[Composite]` vec-carry altitude. Peer at the inner
2845 /// altitude of [`crate::upgrade::UpgradeFromEntry::instructions`]
2846 /// (0137e5a) — same "one typed dispatch on the substrate
2847 /// primitive, thin projections at each consumer" discipline
2848 /// folded onto the outer top-level [`Caixa`] altitude, opening the
2849 /// M2 vec-carry slot family's outer-composite-slice axis. Sibling
2850 /// in shape to the peer outer-`Caixa` `&[Dep]`-return
2851 /// [`Self::deps`] (ad34b4e) / [`Self::deps_dev`] (f7fd81e) and
2852 /// `&[String]`-return [`Self::autores`] (b5d813f) /
2853 /// [`Self::etiquetas`] (78c7d3c) / [`Self::bibliotecas`]
2854 /// (8a36c23) / [`Self::exe`] (65d9527) / [`Self::servicos`]
2855 /// (611f78b) slice-accessors on the sibling outer-`Caixa` scalar-
2856 /// element vec-carry axes — folds the "outer [`Caixa`] `&[T]`
2857 /// slice" projection pattern onto the sibling M2 typed-composite-
2858 /// element axis (`UpgradeFromEntry` composite, matching the
2859 /// per-inner [`UpgradeFromEntry::instructions`] element type at a
2860 /// different altitude).
2861 ///
2862 /// Returns `&[UpgradeFromEntry]` (not `&Vec<UpgradeFromEntry>`)
2863 /// because every downstream consumer of the hot-upgrade list
2864 /// treats it as a read-only sequence — the slice-view is the
2865 /// narrowest borrow that supports every present + roadmapped
2866 /// consumer (`.iter()`, `.len()`, `.is_empty()`, `serde` slice-
2867 /// serialization through
2868 /// `serde_yaml::to_value(&[UpgradeFromEntry])`) without leaking
2869 /// the backing `Vec`'s grow/push/reserve surface no consumer of
2870 /// the typed view reaches for (the storage-side `Vec` remains
2871 /// reachable through the `pub upgrade_from` field for the
2872 /// mutation-carrying serde round-trip and per-test fixture-
2873 /// mutation paths). Named `upgrade_from()` to match the storage
2874 /// field's `snake_case` name; the kebab-case author-surface tag
2875 /// `:upgrade-from` is the same axis after tatara-lisp's
2876 /// kebab↔snake fold and the accessor's identity maps onto the
2877 /// canonical CAIXA-SDLC §II vocabulary the slot's docstring
2878 /// already carries.
2879 #[must_use]
2880 pub fn upgrade_from(&self) -> &[UpgradeFromEntry] {
2881 self.upgrade_from.as_slice()
2882 }
2883
2884 /// Substrate-canonical per-`Caixa` `:children` M2 supervisor-tree-
2885 /// slot outer-composite OTP-shaped per-supervisor static-child-list
2886 /// slice accessor every consumer of the top-level manifest's per-
2887 /// Supervisor `&[ChildSpec]` slice-view keys off — returns the
2888 /// author-declared `:children` typed `Vec<crate::supervisor::ChildSpec>`
2889 /// verbatim as a `&[crate::supervisor::ChildSpec]` slice-view over
2890 /// the same backing buffer the raw `self.children.as_slice()` field
2891 /// access borrows from. Empty-slice-carrying (the "no static children
2892 /// declared" arm every non-`Supervisor`-kind `defcaixa` carries by
2893 /// #[serde(default)] and every `SimpleOneForOne` supervisor carries
2894 /// by [`crate::supervisor::SupervisorError::SimpleOneForOneWithStaticChildren`]
2895 /// gate; the returned `&[ChildSpec]` degenerates to an empty slice
2896 /// on those arms without any silent `None` collapse).
2897 ///
2898 /// The outer `:children` slot carries the M2 typed OTP-supervisor
2899 /// static-child list — the load-bearing container of every per-
2900 /// child `{caixa, versao, restart}` triple the wasm-operator's
2901 /// hierarchical reconciler dispatches on at supervisor-tree
2902 /// materialization time (INSPIRATIONS §II.2 — OTP `supervisor:init/1`
2903 /// static-child list translated onto pleme-io's typed
2904 /// [`crate::supervisor::ChildSpec`] entry list; CAIXA-SDLC §II —
2905 /// the typed-M2 slot algebra the operator's per-supervisor fan-out
2906 /// dispatch fans on). Every per-child axis threads through a lifted
2907 /// per-entry accessor on the [`crate::supervisor::ChildSpec`] type:
2908 /// the [`crate::supervisor::ChildSpec::nome`] DNS-1123-label
2909 /// child-caixa-identity scalar accessor, the peer versao SemVer-2
2910 /// version-requirement scalar accessor, and the
2911 /// [`crate::supervisor::ChildSpec::restart`] `Copy`-composite-enum
2912 /// per-child post-exit restart-decision-policy discriminant
2913 /// accessor (dfb4a81). Every downstream consumer of the supervisor-
2914 /// tree path first passes through this outer accessor onto the
2915 /// slice and then dispatches per-child through the inner accessors
2916 /// — the two-level dispatch means every per-`:children` reader now
2917 /// routes through a typed dispatch on the substrate primitive at
2918 /// both altitudes.
2919 ///
2920 /// Prior to this lift the `.children` `Vec<ChildSpec>` slot was
2921 /// accessed inline at three production sites across two files —
2922 /// the [`Self::declared_supervisor_slots`] supervisor-tree
2923 /// declared-slot enumerator's `!self.children.is_empty()` presence
2924 /// probe (caixa-core/src/manifest.rs, which drives the
2925 /// `SUPERVISOR_AUTHOR_KEY_CHILDREN` kebab-case author-label push
2926 /// every [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
2927 /// kind-coherence gate reads), the [`Self::supervisor_view`]
2928 /// per-supervisor typed-view composer's `self.children.clone()`
2929 /// per-child fold-in path (caixa-core/src/manifest.rs, which
2930 /// materializes the typed [`crate::supervisor::SupervisorSpec`]
2931 /// view every [`crate::StandardLayout::verify`] Supervisor-arm gate
2932 /// dispatches on), and the [`crate::StandardLayout::verify`] per-
2933 /// `:children :caixa` self-parent refusal probe's
2934 /// `&caixa.children`-borrowed
2935 /// [`crate::supervisor::validate_no_self_supervision`] input
2936 /// (caixa-core/src/layout.rs, which pins the "no child names the
2937 /// supervisor's own `:nome`" cross-slot coherence gate). A future
2938 /// extension of the outer `:children` axis (a per-cluster
2939 /// `:children-overrides` overlay the wasm-engine operator resolves
2940 /// at admission time so a cluster-specific child-set can tighten
2941 /// a caixa-declared list without re-authoring the `caixa.lisp`,
2942 /// promotion of the plain `Vec<ChildSpec>` to a richer
2943 /// `{static, dynamic}` partition once Erlang/OTP's
2944 /// `simple_one_for_one`-shaped dynamic-child slot lands as a typed
2945 /// axis, per-child priority annotation once multi-strategy fan-out
2946 /// lands) would have had to be threaded through all three open-
2947 /// coded copies in lockstep or one consumer would silently
2948 /// disagree with the peers on which child slice a given Caixa
2949 /// resolves to — the enumerator's presence probe reading the raw
2950 /// slot while the peer view-composer's fold-in path read an
2951 /// operator-resolved slot would silently split the paired
2952 /// declared-slot enumerator and typed-view composition, and the
2953 /// [`crate::supervisor::validate_no_self_supervision`] self-parent
2954 /// refusal probe reading a third borrow would silently drift the
2955 /// cross-slot coherence gate's traversal input from the two peers,
2956 /// a three-consumer split at the enumerator, the view composer,
2957 /// and the self-parent gate far from the source `caixa.lisp` with
2958 /// no field naming the child-set-drift root cause. Lifting the
2959 /// resolution rule to a typed method on the substrate primitive
2960 /// means every downstream consumer of the caixa's per-`Caixa`
2961 /// OTP-supervisor outer-slice surface reaches for exactly one
2962 /// typed dispatch — the resolver's accept-set migrates as a unit
2963 /// on any future axis addition.
2964 ///
2965 /// Second outer top-level [`Caixa`] `&[Composite]`-return slice
2966 /// accessor for M2 / M3 typed-slot vec-carry axes — folds on the
2967 /// outer-`Caixa` `&[Composite]` composite-slice sub-family the
2968 /// sibling [`Self::upgrade_from`] (2a1f907) accessor opened, peer
2969 /// at the outer altitude of the closed inner-`SupervisorSpec`
2970 /// [`crate::SupervisorSpec::children`] (bc92bce) accessor on the
2971 /// same OTP-supervisor static-child-list axis — same "byte-equal,
2972 /// borrow-shared" outer-accessor discipline extended onto the
2973 /// second outer-`Caixa` `&[Composite]` vec-carry axis. Sibling in
2974 /// shape to the peer outer-`Caixa` `&[Dep]`-return [`Self::deps`]
2975 /// (ad34b4e) / [`Self::deps_dev`] (f7fd81e) and `&[String]`-return
2976 /// [`Self::autores`] (b5d813f) / [`Self::etiquetas`] (78c7d3c) /
2977 /// [`Self::bibliotecas`] (8a36c23) / [`Self::exe`] (65d9527) /
2978 /// [`Self::servicos`] (611f78b) slice-accessors on the sibling
2979 /// outer-`Caixa` scalar-element vec-carry axes — folds the "outer
2980 /// [`Caixa`] `&[T]` slice" projection pattern onto the sibling
2981 /// M2 typed-composite-element axis
2982 /// ([`crate::supervisor::ChildSpec`] composite, matching the
2983 /// per-inner [`crate::SupervisorSpec::children`] element type at a
2984 /// different altitude).
2985 ///
2986 /// Returns `&[crate::supervisor::ChildSpec]` (not
2987 /// `&Vec<ChildSpec>`) because every downstream consumer of the
2988 /// child list treats it as a read-only sequence — the slice-view
2989 /// is the narrowest borrow that supports every present +
2990 /// roadmapped consumer (`.iter()`, `.len()`, `.is_empty()`, the
2991 /// [`crate::supervisor::validate_no_self_supervision`] `&[ChildSpec]`
2992 /// input, `serde` slice-serialization) without leaking the backing
2993 /// `Vec`'s grow/push/reserve surface no consumer of the typed view
2994 /// reaches for (the storage-side `Vec` remains reachable through
2995 /// the `pub children` field for the mutation-carrying serde round-
2996 /// trip and per-test fixture-mutation paths, including the
2997 /// [`Self::supervisor_view`] fold-in path that clones the slot
2998 /// into the typed view). Named `children()` to match the storage
2999 /// field's name verbatim and the tatara-lisp author-surface term
3000 /// (`:children`) the field's own docstring already carries; the
3001 /// accessor's identity maps onto the canonical OTP supervision
3002 /// vocabulary the [`Caixa::children`] field's docstring already
3003 /// reaches for ("Static children of a supervisor").
3004 #[must_use]
3005 pub fn children(&self) -> &[crate::supervisor::ChildSpec] {
3006 self.children.as_slice()
3007 }
3008
3009 /// Substrate-canonical per-`Caixa` `:membros` M3 mesh-slot outer-
3010 /// composite MESH-COMPOSITION-shaped per-Aplicacao member-list slice
3011 /// accessor every consumer of the top-level manifest's per-Aplicacao
3012 /// `&[crate::aplicacao::Membro]` slice-view keys off — returns the
3013 /// author-declared `:membros` typed `Vec<crate::aplicacao::Membro>`
3014 /// verbatim as a `&[crate::aplicacao::Membro]` slice-view over the
3015 /// same backing buffer the raw `self.membros.as_slice()` field access
3016 /// borrows from. Empty-slice-carrying (the "no members declared" arm
3017 /// every non-`Aplicacao`-kind `defcaixa` carries by `#[serde(default)]`
3018 /// and every partially-authored Aplicacao carries before the
3019 /// [`crate::AplicacaoError::MembrosEmpty`] gate fires; the returned
3020 /// `&[Membro]` degenerates to an empty slice on those arms without any
3021 /// silent `None` collapse).
3022 ///
3023 /// The outer `:membros` slot carries the M3 typed MESH-COMPOSITION
3024 /// per-Aplicacao member list — the load-bearing container of every
3025 /// per-member `{caixa, versao}` pair the caixa-mesh renderer's
3026 /// per-Aplicacao program-emission dispatch fans on at mesh-artifact
3027 /// materialization time (MESH-COMPOSITION §III.1 — the typed graph's
3028 /// vertex set the `:contratos` `:de`/`:para` edges resolve against and
3029 /// the `:entrada :para` external-gateway destination validates
3030 /// against; CAIXA-SDLC §II — the typed-M3 slot algebra the operator's
3031 /// per-Aplicacao fan-out dispatch fans on). Every per-member axis
3032 /// threads through a lifted per-entry accessor on the
3033 /// [`crate::aplicacao::Membro`] type: the
3034 /// [`crate::aplicacao::Membro::nome`] DNS-1123-label member-caixa-
3035 /// identity scalar accessor (4a32abf) and the peer
3036 /// [`crate::aplicacao::Membro::versao_requirement`] SemVer-2
3037 /// version-requirement scalar accessor (a40b0e3). Every downstream
3038 /// consumer of the mesh-graph path first passes through this outer
3039 /// accessor onto the slice and then dispatches per-member through
3040 /// the inner accessors — the two-level dispatch means every per-
3041 /// `:membros` reader now routes through a typed dispatch on the
3042 /// substrate primitive at both altitudes.
3043 ///
3044 /// Prior to this lift the `.membros` `Vec<Membro>` slot was accessed
3045 /// inline at three production sites across two files — the
3046 /// [`Self::declared_mesh_slots`] mesh-slot declared-slot
3047 /// enumerator's `!self.membros.is_empty()` presence probe
3048 /// (caixa-core/src/manifest.rs, which drives the
3049 /// `M3_AUTHOR_KEY_MEMBROS` kebab-case author-label push every
3050 /// [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-coherence
3051 /// gate reads), the [`Self::aplicacao_view`] per-Aplicacao typed-view
3052 /// composer's `self.membros.clone()` per-member fold-in path
3053 /// (caixa-core/src/manifest.rs, which materializes the typed
3054 /// [`crate::aplicacao::AplicacaoSpec`] view every
3055 /// [`crate::StandardLayout::verify`] Aplicacao-arm gate dispatches
3056 /// on), and the [`crate::StandardLayout::verify`] per-`:membros
3057 /// :caixa` self-membership refusal probe's `&caixa.membros`-borrowed
3058 /// [`crate::aplicacao::validate_no_self_membership`] input
3059 /// (caixa-core/src/layout.rs, which pins the "no member names the
3060 /// Aplicacao's own `:nome`" cross-slot coherence gate). A future
3061 /// extension of the outer `:membros` axis (a per-cluster
3062 /// `:membros-overrides` overlay the wasm-engine operator resolves at
3063 /// admission time so a cluster-specific member-set can tighten a
3064 /// caixa-declared list without re-authoring the `caixa.lisp`,
3065 /// promotion of the plain `Vec<Membro>` to a richer
3066 /// `{static, dynamic}` partition once runtime-resolved Aplicacao
3067 /// members land as a typed axis, per-member priority annotation once
3068 /// multi-strategy fan-out lands) would have had to be threaded
3069 /// through all three open-coded copies in lockstep or one consumer
3070 /// would silently disagree with the peers on which member slice a
3071 /// given Caixa resolves to — the enumerator's presence probe reading
3072 /// the raw slot while the peer view-composer's fold-in path read an
3073 /// operator-resolved slot would silently split the paired
3074 /// declared-slot enumerator and typed-view composition, and the
3075 /// [`crate::aplicacao::validate_no_self_membership`] self-membership
3076 /// refusal probe reading a third borrow would silently drift the
3077 /// cross-slot coherence gate's traversal input from the two peers, a
3078 /// three-consumer split at the enumerator, the view composer, and
3079 /// the self-membership gate far from the source `caixa.lisp` with no
3080 /// field naming the member-set-drift root cause. Lifting the
3081 /// resolution rule to a typed method on the substrate primitive
3082 /// means every downstream consumer of the caixa's per-`Caixa`
3083 /// MESH-COMPOSITION outer-slice surface reaches for exactly one
3084 /// typed dispatch — the resolver's accept-set migrates as a unit on
3085 /// any future axis addition.
3086 ///
3087 /// Third outer top-level [`Caixa`] `&[Composite]`-return slice
3088 /// accessor for M2 / M3 typed-slot vec-carry axes — opens the outer-
3089 /// `Caixa` M3 mesh-slot arm of the `&[Composite]` composite-slice
3090 /// sub-family the sibling M2 [`Self::upgrade_from`] (2a1f907) /
3091 /// [`Self::children`] (c17b51e) accessors opened for the M2 vec-carry
3092 /// altitude. Peer at the outer altitude of the closed inner-
3093 /// [`crate::AplicacaoSpec::membros`] (6c77e36) accessor on the same
3094 /// MESH-COMPOSITION per-Aplicacao member-list axis — the two
3095 /// altitudes now share the same "byte-equal, borrow-shared" outer-
3096 /// accessor discipline. Sibling in shape to the peer outer-`Caixa`
3097 /// `&[Dep]`-return [`Self::deps`] (ad34b4e) / [`Self::deps_dev`]
3098 /// (f7fd81e) and `&[String]`-return [`Self::autores`] (b5d813f) /
3099 /// [`Self::etiquetas`] (78c7d3c) / [`Self::bibliotecas`] (8a36c23) /
3100 /// [`Self::exe`] (65d9527) / [`Self::servicos`] (611f78b) slice-
3101 /// accessors on the sibling outer-`Caixa` scalar-element vec-carry
3102 /// axes — folds the "outer [`Caixa`] `&[T]` slice" projection
3103 /// pattern onto the sibling M3 typed-composite-element axis
3104 /// ([`crate::aplicacao::Membro`] composite, matching the per-inner
3105 /// [`crate::AplicacaoSpec::membros`] element type at a different
3106 /// altitude).
3107 ///
3108 /// Returns `&[crate::aplicacao::Membro]` (not `&Vec<Membro>`)
3109 /// because every downstream consumer of the member list treats it
3110 /// as a read-only sequence — the slice-view is the narrowest borrow
3111 /// that supports every present + roadmapped consumer (`.iter()`,
3112 /// `.len()`, `.is_empty()`, the
3113 /// [`crate::aplicacao::validate_no_self_membership`] `&[Membro]`
3114 /// input, `serde` slice-serialization) without leaking the backing
3115 /// `Vec`'s grow/push/reserve surface no consumer of the typed view
3116 /// reaches for (the storage-side `Vec` remains reachable through the
3117 /// `pub membros` field for the mutation-carrying serde round-trip
3118 /// and per-test fixture-mutation paths, including the
3119 /// [`Self::aplicacao_view`] fold-in path that clones the slot into
3120 /// the typed view). Named `membros()` to match the storage field's
3121 /// name verbatim and the tatara-lisp author-surface term
3122 /// (`:membros`) the field's own docstring already carries; the
3123 /// accessor's identity maps onto the canonical MESH-COMPOSITION
3124 /// vocabulary the [`Caixa::membros`] field's docstring already
3125 /// reaches for ("Member Servicos that make up this Aplicacao").
3126 #[must_use]
3127 pub fn membros(&self) -> &[crate::aplicacao::Membro] {
3128 self.membros.as_slice()
3129 }
3130
3131 /// Substrate-canonical per-`Caixa` `:contratos` M3 mesh-slot outer-
3132 /// composite MESH-COMPOSITION-shaped per-Aplicacao WIT-typed
3133 /// inter-Servico contract-list slice accessor every consumer of the
3134 /// top-level manifest's per-Aplicacao `&[crate::aplicacao::WitContract]`
3135 /// slice-view keys off — returns the author-declared `:contratos`
3136 /// typed `Vec<crate::aplicacao::WitContract>` verbatim as a
3137 /// `&[crate::aplicacao::WitContract]` slice-view over the same
3138 /// backing buffer the raw `self.contratos.as_slice()` field access
3139 /// borrows from. Empty-slice-carrying (the "no contracts declared"
3140 /// arm every non-`Aplicacao`-kind `defcaixa` carries by
3141 /// `#[serde(default)]` and every leaf Aplicacao carrying only a
3142 /// single member with no inter-Servico edge carries; the returned
3143 /// `&[WitContract]` degenerates to an empty slice on those arms
3144 /// without any silent `None` collapse).
3145 ///
3146 /// The outer `:contratos` slot carries the M3 typed MESH-COMPOSITION
3147 /// per-Aplicacao WIT-typed inter-Servico edge list — the load-bearing
3148 /// container of every per-edge `{de, para, wit, endpoint | subject |
3149 /// slot}` quadruple the caixa-mesh renderer's per-Aplicacao
3150 /// `CiliumNetworkPolicy` fan-out (one L7 policy per edge —
3151 /// MESH-COMPOSITION §III.2 point 2) and per-`(:de, :para)`
3152 /// adjacency-list seed dispatch on at mesh-artifact materialization
3153 /// time (MESH-COMPOSITION §III.1 — the typed graph's edge set the
3154 /// `:membros` vertex set resolves against, closed by the
3155 /// [`crate::AplicacaoError::ContractoUnknownMember`] / cycle-refusal
3156 /// gates in §III.3; CAIXA-SDLC §II — the typed-M3 slot algebra the
3157 /// operator's per-Aplicacao fan-out dispatch fans on). Every
3158 /// per-edge axis threads through a lifted per-entry accessor on the
3159 /// [`crate::aplicacao::WitContract`] type: the peer `de` / `para`
3160 /// DNS-1123-label member-caixa-name endpoint scalar accessors, the
3161 /// [`crate::aplicacao::WitContract::endpoint`] (7020470) HTTP-shape
3162 /// / [`crate::aplicacao::WitContract::subject`] (90de675)
3163 /// NATS-pub-sub-shape / [`crate::aplicacao::WitContract::slot`]
3164 /// (ed22b66) `wasi:keyvalue/store`-shape payload-carrier accessors,
3165 /// and the WIT-world discriminant. Every downstream consumer of the
3166 /// mesh-graph edge path first passes through this outer accessor
3167 /// onto the slice and then dispatches per-contract through the
3168 /// inner accessors — the two-level dispatch means every
3169 /// per-`:contratos` reader now routes through a typed dispatch on
3170 /// the substrate primitive at both altitudes.
3171 ///
3172 /// Prior to this lift the `.contratos` `Vec<WitContract>` slot was
3173 /// accessed inline at two production sites in
3174 /// caixa-core/src/manifest.rs — the [`Self::declared_mesh_slots`]
3175 /// mesh-slot declared-slot enumerator's
3176 /// `!self.contratos.is_empty()` presence probe (which drives the
3177 /// `M3_AUTHOR_KEY_CONTRATOS` kebab-case author-label push every
3178 /// [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-coherence
3179 /// gate reads) and the [`Self::aplicacao_view`] per-Aplicacao
3180 /// typed-view composer's `self.contratos.clone()` per-contract
3181 /// fold-in path (which materializes the typed
3182 /// [`crate::aplicacao::AplicacaoSpec`] view every
3183 /// [`crate::StandardLayout::verify`] Aplicacao-arm gate and every
3184 /// downstream `caixa-mesh` renderer dispatches on). A future
3185 /// extension of the outer `:contratos` axis (a per-cluster
3186 /// `:contratos-overrides` overlay the wasm-engine operator resolves
3187 /// at admission time so a cluster-specific edge-set can tighten a
3188 /// caixa-declared list without re-authoring the `caixa.lisp`,
3189 /// promotion of the plain `Vec<WitContract>` to a richer
3190 /// `{static, dynamic}` partition once runtime-resolved contract
3191 /// edges land, per-edge policy annotation once the M4 per-edge
3192 /// policy overlay axis lands) would have had to be threaded through
3193 /// both open-coded copies in lockstep or one consumer would
3194 /// silently disagree with the peer on which edge slice a given
3195 /// Caixa resolves to — the enumerator's presence probe reading the
3196 /// raw slot while the peer view-composer's fold-in path read an
3197 /// operator-resolved slot would silently split the paired
3198 /// declared-slot enumerator and typed-view composition, a
3199 /// two-consumer split at the enumerator and the view composer far
3200 /// from the source `caixa.lisp` with no field naming the edge-set-
3201 /// drift root cause. Lifting the resolution rule to a typed method
3202 /// on the substrate primitive means every downstream consumer of
3203 /// the caixa's per-`Caixa` MESH-COMPOSITION outer-slice surface
3204 /// reaches for exactly one typed dispatch — the resolver's
3205 /// accept-set migrates as a unit on any future axis addition.
3206 ///
3207 /// Fourth and final outer top-level [`Caixa`] `&[Composite]`-return
3208 /// slice accessor for M2 / M3 typed-slot vec-carry axes — closes
3209 /// the outer-`Caixa` `&[Composite]` composite-slice sub-family the
3210 /// sibling M2 [`Self::upgrade_from`] (2a1f907) / [`Self::children`]
3211 /// (c17b51e) accessors opened and the M3 [`Self::membros`]
3212 /// (0f26987) accessor folded on, and closes the outer-`Caixa` M3
3213 /// mesh-slot arm of the composite-slice sub-family the sibling
3214 /// [`Self::membros`] accessor opened for the M3 vec-carry altitude.
3215 /// Peer at the outer altitude of the closed inner-
3216 /// [`crate::AplicacaoSpec::contratos`] (0dcc926) accessor on the
3217 /// same MESH-COMPOSITION per-Aplicacao contract-list axis — the two
3218 /// altitudes now share the same "byte-equal, borrow-shared" outer-
3219 /// accessor discipline. Sibling in shape to the peer outer-`Caixa`
3220 /// `&[Dep]`-return [`Self::deps`] (ad34b4e) / [`Self::deps_dev`]
3221 /// (f7fd81e) and `&[String]`-return [`Self::autores`] (b5d813f) /
3222 /// [`Self::etiquetas`] (78c7d3c) / [`Self::bibliotecas`] (8a36c23) /
3223 /// [`Self::exe`] (65d9527) / [`Self::servicos`] (611f78b) slice-
3224 /// accessors on the sibling outer-`Caixa` scalar-element vec-carry
3225 /// axes — folds the "outer [`Caixa`] `&[T]` slice" projection
3226 /// pattern onto the sibling M3 typed-composite-element axis
3227 /// ([`crate::aplicacao::WitContract`] composite, matching the
3228 /// per-inner [`crate::AplicacaoSpec::contratos`] element type at a
3229 /// different altitude).
3230 ///
3231 /// Returns `&[crate::aplicacao::WitContract]` (not
3232 /// `&Vec<WitContract>`) because every downstream consumer of the
3233 /// contract list treats it as a read-only sequence — the slice-view
3234 /// is the narrowest borrow that supports every present + roadmapped
3235 /// consumer (`.iter()`, `.len()`, `.is_empty()`, per-edge WIT-world
3236 /// discriminant dispatch, `serde` slice-serialization) without
3237 /// leaking the backing `Vec`'s grow/push/reserve surface no
3238 /// consumer of the typed view reaches for (the storage-side `Vec`
3239 /// remains reachable through the `pub contratos` field for the
3240 /// mutation-carrying serde round-trip and per-test fixture-mutation
3241 /// paths, including the [`Self::aplicacao_view`] fold-in path that
3242 /// clones the slot into the typed view). Named `contratos()` to
3243 /// match the storage field's name verbatim and the tatara-lisp
3244 /// author-surface term (`:contratos`) the field's own docstring
3245 /// already carries; the accessor's identity maps onto the canonical
3246 /// MESH-COMPOSITION vocabulary the [`Caixa::contratos`] field's
3247 /// docstring already reaches for ("WIT-typed inter-Servico
3248 /// contracts").
3249 #[must_use]
3250 pub fn contratos(&self) -> &[crate::aplicacao::WitContract] {
3251 self.contratos.as_slice()
3252 }
3253
3254 /// Compose the Aplicacao-related flat slots into a single typed
3255 /// [`crate::aplicacao::AplicacaoSpec`] for validation +
3256 /// downstream renderer consumption. Returns `None` when the
3257 /// caixa isn't a `:kind Aplicacao`.
3258 #[must_use]
3259 pub fn aplicacao_view(&self) -> Option<crate::aplicacao::AplicacaoSpec> {
3260 if !self.kind().is_aplicacao() {
3261 return None;
3262 }
3263 Some(crate::aplicacao::AplicacaoSpec {
3264 membros: self.membros().to_vec(),
3265 contratos: self.contratos().to_vec(),
3266 politicas: self.politicas().cloned().unwrap_or_default(),
3267 placement: self.placement().cloned().unwrap_or_default(),
3268 entrada: self.entrada().cloned(),
3269 })
3270 }
3271
3272 /// The kebab-case `:slot` tags of every M3 mesh slot this caixa
3273 /// *declares* a value on, in canonical declaration order
3274 /// (`:membros` → `:contratos` → `:politicas` → `:placement` →
3275 /// `:entrada`). A slot counts as declared when its backing field
3276 /// carries a value — a non-empty `Vec`, or a `Some(...)`.
3277 ///
3278 /// The M3 mesh slots compose the typed graph of a `:kind Aplicacao`
3279 /// (MESH-COMPOSITION §III.1). [`Self::aplicacao_view`] only folds
3280 /// them into a validatable [`crate::aplicacao::AplicacaoSpec`] when
3281 /// the kind matches (returns `None` otherwise), and the caixa-mesh /
3282 /// caixa-flux / caixa-helm renderers only emit them for an
3283 /// Aplicacao. On any *other* kind a declared mesh slot is the
3284 /// manifest field's documented "ignored otherwise" (see the
3285 /// `:membros` … `:entrada` field docs): it silently passes
3286 /// [`Caixa::from_lisp`] and then vanishes — never validated, never
3287 /// rendered — far from the source caixa.lisp.
3288 /// [`crate::StandardLayout::verify`] consults this to reject that
3289 /// silent-drop at caixa-build time
3290 /// ([`crate::LayoutError::MeshSlotsOnNonAplicacao`]), mirroring the
3291 /// `SupervisorOwnsCode` / `AplicacaoOwnsCode` kind-coherence gates:
3292 /// a slot foreign to the kind is a build error, not a silent drop.
3293 ///
3294 /// Lifted as a typed method (rather than an inline disjunction at
3295 /// the verify call site) so the mesh-slot set lives in one place —
3296 /// a future M4 axis added to the Aplicacao surface (per-edge policy
3297 /// overlay, distributed-app takeover config) is one push here, and
3298 /// every consumer reaching for "which mesh slots are set" (the
3299 /// verify gate, a future `feira lint` kind-coherence advisory)
3300 /// inherits the canonical order without rolling its own.
3301 ///
3302 /// Each per-arm kebab-case label is routed through the peer
3303 /// [`crate::M3_AUTHOR_KEY_MEMBROS`] /
3304 /// [`crate::M3_AUTHOR_KEY_CONTRATOS`] /
3305 /// [`crate::M3_AUTHOR_KEY_POLITICAS`] /
3306 /// [`crate::M3_AUTHOR_KEY_PLACEMENT`] /
3307 /// [`crate::M3_AUTHOR_KEY_ENTRADA`] consts declared next to the
3308 /// [`crate::M3_KEY_PLACEMENT`] renderer-side wire-key peer, so both
3309 /// halves of every M3 top-level mesh slot's dual axis (author-facing
3310 /// kebab-case label + renderer-side artifact key) route through one
3311 /// canonical declaration per arm — same discipline the peer
3312 /// [`crate::M2_AUTHOR_KEY_LIMITS`] / [`crate::M2_AUTHOR_KEY_BEHAVIOR`]
3313 /// / [`crate::M2_AUTHOR_KEY_UPGRADE_FROM`] top-level M2 slot consts
3314 /// (f49c8b0) establish on the sibling per-Servico M2 top-level slot
3315 /// axis, extended here to close the M3 mesh-slot author-facing-label
3316 /// axis so both altitudes of the typed-slot algebra
3317 /// (per-Servico M2 + per-Aplicacao M3) share the same
3318 /// "one canonical byte-string per arm, next to the axis" discipline.
3319 #[must_use]
3320 pub fn declared_mesh_slots(&self) -> Vec<&'static str> {
3321 let mut slots = Vec::new();
3322 if !self.membros().is_empty() {
3323 slots.push(crate::render::M3_AUTHOR_KEY_MEMBROS);
3324 }
3325 if !self.contratos().is_empty() {
3326 slots.push(crate::render::M3_AUTHOR_KEY_CONTRATOS);
3327 }
3328 if self.politicas().is_some() {
3329 slots.push(crate::render::M3_AUTHOR_KEY_POLITICAS);
3330 }
3331 if self.placement().is_some() {
3332 slots.push(crate::render::M3_AUTHOR_KEY_PLACEMENT);
3333 }
3334 if self.entrada().is_some() {
3335 slots.push(crate::render::M3_AUTHOR_KEY_ENTRADA);
3336 }
3337 slots
3338 }
3339
3340 /// The kebab-case `:slot` tags of every supervisor-tree slot this
3341 /// caixa *declares* a value on, in canonical declaration order
3342 /// (`:estrategia` → `:max-restarts` → `:restart-window` →
3343 /// `:children`). A slot counts as declared when its backing field
3344 /// carries a value — a `Some(...)`, or a non-empty `Vec`.
3345 ///
3346 /// The supervisor-tree slots compose the typed OTP supervisor of a
3347 /// `:kind Supervisor` (INSPIRATIONS §II.2; the `:estrategia` +
3348 /// `:children` field docs above). [`Self::supervisor_view`] only
3349 /// folds them into a validatable [`SupervisorSpec`] when the kind
3350 /// matches (returns `None` otherwise), and the wasm-operator's
3351 /// hierarchical reconciler only consumes them for a Supervisor. On
3352 /// any *other* kind a declared supervisor slot is the manifest
3353 /// field's documented "ignored otherwise" (see the `:estrategia` …
3354 /// `:children` field docs): it silently passes [`Caixa::from_lisp`]
3355 /// and then vanishes — never validated, never reconciled — far from
3356 /// the source caixa.lisp. [`crate::StandardLayout::verify`] consults
3357 /// this to reject that silent-drop at caixa-build time
3358 /// ([`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]), the
3359 /// exact mirror of the [`Self::declared_mesh_slots`] /
3360 /// [`crate::LayoutError::MeshSlotsOnNonAplicacao`] gate on the
3361 /// Aplicacao-only slot set: a slot foreign to the kind is a build
3362 /// error, not a silent drop.
3363 #[must_use]
3364 pub fn declared_supervisor_slots(&self) -> Vec<&'static str> {
3365 let mut slots = Vec::new();
3366 if self.estrategia().is_some() {
3367 slots.push(crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA);
3368 }
3369 if self.max_restarts().is_some() {
3370 slots.push(crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS);
3371 }
3372 if self.restart_window().is_some() {
3373 slots.push(crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW);
3374 }
3375 if !self.children().is_empty() {
3376 slots.push(crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN);
3377 }
3378 slots
3379 }
3380
3381 /// The kebab-case `:slot` tags of every M2 Servico-runtime slot this
3382 /// caixa *declares* a value on, in canonical declaration order
3383 /// (`:limits` → `:behavior` → `:upgrade-from`). A slot counts as
3384 /// declared when its backing field carries a value — a `Some(...)`,
3385 /// or a non-empty `Vec`.
3386 ///
3387 /// The M2 slots configure the runtime of a long-running wasm
3388 /// component, i.e. a `:kind Servico`: `:limits` is Lunatic
3389 /// per-process sandboxing (INSPIRATIONS §III.1), `:behavior` is the
3390 /// OTP `gen_server` callback set (§II.3), `:upgrade-from` is the OTP
3391 /// appup hot-code-reload table (§II.4). The caixa-helm / caixa-flux
3392 /// renderers gate on [`crate::require_kind`]`(_, Servico)` and only
3393 /// emit these slots for a Servico; on any *other* kind a declared M2
3394 /// slot is the manifest field's documented "ignored otherwise": its
3395 /// well-formedness is checked by [`crate::StandardLayout::verify`]
3396 /// but the value is never rendered into a chart / programs.yaml entry
3397 /// — it silently passes [`Caixa::from_lisp`] + `feira build` and then
3398 /// vanishes, far from the source caixa.lisp.
3399 /// [`crate::StandardLayout::verify`] consults this to reject that
3400 /// silent-drop at caixa-build time
3401 /// ([`crate::LayoutError::ServicoSlotsOnNonServico`]), the exact
3402 /// mirror of the [`Self::declared_mesh_slots`] /
3403 /// [`Self::declared_supervisor_slots`] gates on the peer
3404 /// kind-exclusive slot sets: a slot foreign to the kind is a build
3405 /// error, not a silent drop.
3406 ///
3407 /// Each per-arm kebab-case label is routed through the peer
3408 /// [`crate::M2_AUTHOR_KEY_LIMITS`] / [`crate::M2_AUTHOR_KEY_BEHAVIOR`] /
3409 /// [`crate::M2_AUTHOR_KEY_UPGRADE_FROM`] consts declared next to the
3410 /// [`crate::M2_KEY_LIMITS`] / [`crate::M2_KEY_BEHAVIOR`] /
3411 /// [`crate::M2_KEY_UPGRADE_FROM`] renderer-side wire-key peers, so
3412 /// both halves of the M2 top-level slot's dual axis (author-facing
3413 /// kebab-case label + renderer-side camelCase overlay-container wire
3414 /// key) route through one canonical declaration per arm — same
3415 /// discipline the peer [`crate::M2_BEHAVIOR_AUTHOR_KEY_ON_*`] sub-slot
3416 /// author-label consts (889dc18) establish on the sibling
3417 /// per-callback axis inside the `:behavior` overlay block.
3418 #[must_use]
3419 pub fn declared_servico_slots(&self) -> Vec<&'static str> {
3420 let mut slots = Vec::new();
3421 if self.limits().is_some() {
3422 slots.push(crate::render::M2_AUTHOR_KEY_LIMITS);
3423 }
3424 if self.behavior().is_some() {
3425 slots.push(crate::render::M2_AUTHOR_KEY_BEHAVIOR);
3426 }
3427 if !self.upgrade_from().is_empty() {
3428 slots.push(crate::render::M2_AUTHOR_KEY_UPGRADE_FROM);
3429 }
3430 slots
3431 }
3432
3433 /// The kebab-case `:slot` tags of every code-surface slot this caixa
3434 /// declares a value on that its [`CaixaKind`] doesn't natively own,
3435 /// in canonical declaration order (`:exe` → `:servicos`). A
3436 /// code-surface slot is owned by exactly one kind: `:exe` by
3437 /// [`CaixaKind::Binario`] (the nix-built executable surface), and
3438 /// `:servicos` by [`CaixaKind::Servico`] (the wasm component +
3439 /// `ComputeUnit` daemon surface).
3440 ///
3441 /// Each is silently ignored when declared on the wrong kind: the
3442 /// caixa-helm / caixa-flux / caixa-flake renderers gate on
3443 /// [`crate::require_kind`]`(_, <owning-kind>)`, so on any *other*
3444 /// code-running kind a declared `:exe` / `:servicos` is the manifest
3445 /// field's documented "ignored otherwise" — its path is checked for
3446 /// existence by the layout's `bibliotecas`/`exe`/`servicos` loops
3447 /// (which run after [`Caixa::from_lisp`]), but the value is never
3448 /// rendered into a build target or programs.yaml entry. It silently
3449 /// passes [`Caixa::from_lisp`] + `feira build`, far from the source
3450 /// caixa.lisp, with no field naming which slot is foreign.
3451 ///
3452 /// [`crate::StandardLayout::verify`] consults this to reject that
3453 /// silent-drop at caixa-build time
3454 /// ([`crate::LayoutError::ForeignCodeSlot`]), beside the M2
3455 /// servico-runtime, supervisor-tree, and M3 mesh kind-coherence
3456 /// gates ([`Self::declared_servico_slots`] /
3457 /// [`Self::declared_supervisor_slots`] /
3458 /// [`Self::declared_mesh_slots`]): the fourth kind ↔ slot algebra
3459 /// axis to be closed on the typed surface. The Supervisor /
3460 /// Aplicacao "no code at all" cases ([`crate::LayoutError::SupervisorOwnsCode`]
3461 /// / [`crate::LayoutError::AplicacaoOwnsCode`]) keep their dedicated
3462 /// diagnostics — they fire ahead of this gate on the same `verify`
3463 /// pass, so for Supervisor / Aplicacao the `OwnCode` arm always wins
3464 /// and this method is moot. For Biblioteca / Binario / Servico, this
3465 /// gate fires when a code-running kind declares another code-running
3466 /// kind's exclusive code surface.
3467 ///
3468 /// `:bibliotecas` is deliberately excluded — a Binario or Servico
3469 /// may legitimately ship a `lib/` helper that the underlying
3470 /// substrate (the nix flake for Binario, the wasm component build
3471 /// for Servico) bundles into its build, so the slot's
3472 /// declared-on-wrong-kind cardinality isn't a structural error on
3473 /// either code-running kind. A Biblioteca declaring `:bibliotecas`
3474 /// is the native case (the slot's owning kind). Supervisor /
3475 /// Aplicacao declaring `:bibliotecas` is gated upstream by
3476 /// [`crate::LayoutError::SupervisorOwnsCode`] /
3477 /// [`crate::LayoutError::AplicacaoOwnsCode`].
3478 ///
3479 /// Lifted as a typed method (rather than an inline disjunction at
3480 /// the verify call site) so the foreign-code-slot set lives in one
3481 /// place — a future kind that gains its own code-surface slot is
3482 /// one push here, and every consumer reaching for "which code
3483 /// surfaces are foreign to this kind" (the verify gate, a future
3484 /// `feira lint` kind-coherence advisory, the future `app-operator`'s
3485 /// per-caixa build-target classifier) inherits the canonical order
3486 /// without rolling its own.
3487 #[must_use]
3488 pub fn declared_foreign_code_slots(&self) -> Vec<&'static str> {
3489 let mut slots = Vec::new();
3490 if !self.exe().is_empty() && !self.kind().requires_exe() {
3491 slots.push(":exe");
3492 }
3493 if !self.servicos().is_empty() && !self.kind().requires_servicos() {
3494 slots.push(":servicos");
3495 }
3496 slots
3497 }
3498
3499 /// Validate every entry of `:deps` and `:deps-dev` through
3500 /// [`Dep::validate`] — closing the parity loop with the per-axis
3501 /// `:versao` gates already wired into the typed-graph
3502 /// ([`crate::AplicacaoSpec::validate_membros`] for `:membros`,
3503 /// 9888b13) and typed supervisor tree
3504 /// ([`crate::SupervisorSpec::validate`] for `:children`, b38ff3a).
3505 ///
3506 /// Until this gate landed `:deps :versao` and `:deps-dev :versao`
3507 /// were the only `:versao` axes still untyped past
3508 /// [`Caixa::from_lisp`]: the derive macro stored the requirement
3509 /// as a String without parsing it, so a malformed-but-non-empty
3510 /// requirement (`"^bad-version"`, `"^^0.1"`, `"v0.1"`, `"not-a-req"`)
3511 /// silently passed parse and the `semver::Error` surfaced at
3512 /// lacre-resolve time, far from the source caixa.lisp, with no
3513 /// field naming which `:deps` entry carried the typo. Lifting the
3514 /// gate here makes the four `:versao` typed surfaces (`:deps`,
3515 /// `:deps-dev`, `:membros`, `:children`) structurally equivalent —
3516 /// every requirement string past `validate_deps` is round-trippable
3517 /// through [`crate::parse_requirement`] without re-checking at the
3518 /// resolver layer.
3519 ///
3520 /// Both lists run through the same per-entry validator so a typo
3521 /// in `:deps-dev` surfaces with the same diagnostic as one in
3522 /// `:deps` — neither axis is a second-class citizen of the typed
3523 /// surface.
3524 ///
3525 /// Within each list, [`DepError::DuplicateNome`] closes the
3526 /// set-not-multiset discipline on the `:nome` axis: two entries
3527 /// naming the same caixa carry two `:versao` / `:fonte` / feature
3528 /// triples that the caixa-resolver's lacre pipeline collapses to one
3529 /// via its `HashMap`-keyed-by-`:nome` consumption — the second entry
3530 /// silently overwrites the first at `concrete_versao`-resolve time
3531 /// (the same "second wins / one silently overwrites the other"
3532 /// shape the peer typed-graph duplicate gates already close on every
3533 /// other Vec-shaped authoring surface that keys by name). The
3534 /// duplicate check fires per-list and runs *after* each per-entry
3535 /// [`Dep::validate`] call so a malformed-and-duplicated entry
3536 /// surfaces its narrower per-entry diagnostic
3537 /// ([`DepError::NomeInvalid`], [`DepError::VersaoInvalid`],
3538 /// [`DepError::FonteRepoEmpty`], …) before the cross-entry duplicate
3539 /// diagnostic — the canonical "per-entry shape before cross-entry
3540 /// uniqueness" precedence the peer `:children :caixa`
3541 /// ([`crate::SupervisorSpec::validate`]), `:membros :caixa`
3542 /// ([`crate::AplicacaoSpec::validate_membros`]), `:contratos`
3543 /// ([`crate::AplicacaoSpec::validate`]), `:placement :clusters`
3544 /// ([`crate::AplicacaoSpec::validate_placement`]),
3545 /// `:entrada :paths` ([`crate::AplicacaoSpec::validate`]),
3546 /// `:upgrade-from :from` ([`crate::upgrade::validate_upgrade_from`]),
3547 /// and the within-`:upgrade-from`-entry per-instruction-class
3548 /// singularity gates ([`crate::UpgradeError::DuplicateLoadModule`],
3549 /// [`crate::UpgradeError::DuplicateStateChange`],
3550 /// [`crate::UpgradeError::DuplicateCleanup`]) all establish.
3551 ///
3552 /// Cross-list (`:deps` ↔ `:deps-dev`) coincidence is *not* gated
3553 /// here: Cargo's `[dependencies]` + `[dev-dependencies]` accept the
3554 /// same name in both tables (the dev table's pin overrides the
3555 /// runtime table's pin in test/dev contexts), and caixa's surface
3556 /// mirrors that convention until a deliberate choice retires the
3557 /// override pattern. Only within-list duplicates are structurally
3558 /// incoherent — those are what this gate closes.
3559 pub fn validate_deps(&self) -> Result<(), DepError> {
3560 let mut seen = std::collections::HashSet::new();
3561 for dep in self.deps() {
3562 dep.validate()?;
3563 crate::render::insert_first_seen(&mut seen, dep.nome(), || DepError::DuplicateNome {
3564 nome: dep.nome().to_string(),
3565 list: crate::render::DEP_AUTHOR_KEY_DEPS,
3566 })?;
3567 }
3568 let mut seen_dev = std::collections::HashSet::new();
3569 for dep in self.deps_dev() {
3570 dep.validate()?;
3571 crate::render::insert_first_seen(&mut seen_dev, dep.nome(), || {
3572 DepError::DuplicateNome {
3573 nome: dep.nome().to_string(),
3574 list: crate::render::DEP_AUTHOR_KEY_DEPS_DEV,
3575 }
3576 })?;
3577 }
3578 Ok(())
3579 }
3580
3581 /// Reject `:nome` values the K8s apiserver would refuse at admission
3582 /// time. The top-level Caixa identity flows directly into every
3583 /// substrate-side artifact's `metadata.name` axis: the
3584 /// `lareira-<nome>` Helm chart name ([`caixa-helm::lib::chart_name`]),
3585 /// the programs.yaml `name:` entry the `lareira-fleet-programs`
3586 /// aggregator keys ComputeUnit derivation off
3587 /// ([`caixa-flux::lib::programs_yaml_entry`]), the
3588 /// `LABEL_APLICACAO` label value carried on every Aplicacao-owned
3589 /// pod and the per-`:contratos` CiliumNetworkPolicy `metadata.name`
3590 /// (`<aplicacao>-<de>-to-<para>`) and the per-`:entrada`
3591 /// `<aplicacao>-<para>` HTTPRoute `metadata.name`
3592 /// ([`caixa-mesh::lib::cilium_network_policies`],
3593 /// [`caixa-mesh::lib::gateway_routes`]), and the default
3594 /// `lib/<nome>.lisp` / `exe/<nome>` layout paths
3595 /// ([`crate::StandardLayout::verify`]). Each K8s apiserver-side
3596 /// schema enforces the DNS-1123 label rule on admission; a
3597 /// structurally invalid `:nome` (`"MyApp"` — the canonical
3598 /// "I copied the display name verbatim" footgun, `"my_app"` — the
3599 /// Python-/Postgres-leak, `"team.app"` — `:nome` is a single label
3600 /// not a subdomain, `"-app"` / `"app-"` — DNS-1123 boundary
3601 /// violations, `"my app"` — the paste-from-doc footgun, `"café"` —
3602 /// IDN must be pre-encoded as Punycode, the 64-byte UUID-shaped
3603 /// over-cap slug) silently passed [`Caixa::from_lisp`] and the
3604 /// failure surfaced at `kubectl apply` time as a `metadata.name:
3605 /// Invalid value` rejection on whichever derived artifact admitted
3606 /// first, far from the source `caixa.lisp` and without any field
3607 /// naming the offending `:nome`.
3608 ///
3609 /// Thin wrapper around [`crate::render::is_dns_1123_label`] (the
3610 /// substrate-side predicate the per-axis name gates already share:
3611 /// `:membros :caixa` 3f9d7a0, `:placement :clusters` 6cbb900,
3612 /// `:children :caixa` 31bfa43) that maps the shared parser-shaped
3613 /// reason into the [`ManifestError::NomeInvalid`] variant, so the
3614 /// diagnostic is self-locating (the offending `:nome` is named
3615 /// verbatim) and the author can grep their `caixa.lisp` for
3616 /// `:nome "<value>"` and fix it in one edit. Same diagnostic shape
3617 /// every per-axis sibling gate already exposes
3618 /// ([`crate::AplicacaoError::MembroCaixaInvalid`],
3619 /// [`crate::AplicacaoError::PlacementClusterInvalid`],
3620 /// [`crate::SupervisorError::ChildCaixaInvalid`]).
3621 ///
3622 /// Empty `:nome` (which [`Caixa::from_lisp`] does not reject — the
3623 /// derive macro stores the raw String) is gated by the narrower
3624 /// [`ManifestError::NomeEmpty`] arm before the predicate is
3625 /// consulted, mirroring the empty-first cascade every per-axis
3626 /// name gate already uses (e.g. `MembroCaixaEmpty` before
3627 /// `MembroCaixaInvalid`, `EmptyChildName` before `ChildCaixaInvalid`).
3628 pub fn validate_nome(&self) -> Result<(), ManifestError> {
3629 // Routes through the shared
3630 // [`crate::render::require_valid_dns_1123_label`] gate the peer
3631 // name axes each land on so drift between the eight axes'
3632 // accepted DNS-1123-label sets is structurally impossible.
3633 let nome = self.nome();
3634 crate::render::require_valid_dns_1123_label(
3635 nome,
3636 || ManifestError::NomeEmpty,
3637 |reason| ManifestError::NomeInvalid {
3638 nome: nome.to_string(),
3639 reason,
3640 },
3641 )
3642 }
3643
3644 /// Reject `:nome` values whose joint length with the canonical
3645 /// [`crate::LAREIRA_CHART_NAME_PREFIX`] (`"lareira-"`) overflows
3646 /// the K8s DNS-1123 label cap [`crate::DNS_1123_LABEL_MAX_LEN`]
3647 /// (63 bytes). Every per-Servico / per-Aplicacao renderer the
3648 /// substrate carries materializes the caixa's `:nome` through the
3649 /// canonical [`crate::lareira_chart_name`] helper (f7320d7) into a
3650 /// `lareira-<nome>` artifact that lands as a K8s `metadata.name` /
3651 /// Helm chart name / `HelmRelease` `release_name`: `caixa-helm`'s
3652 /// `ChartDir.name` + `Chart.yaml::name`
3653 /// (caixa-helm/src/lib.rs:207), `caixa-flux`'s `cluster_bundle`
3654 /// `HelmRelease` `chart:` slot (caixa-flux/src/lib.rs:329),
3655 /// `caixa-tatara`'s `process_for_aplicacao` `release_name` +
3656 /// `oci://<registry>/lareira-<nome>` chart ref
3657 /// (caixa-tatara/src/lib.rs:124,178). Helm's own `Chart.yaml::name`
3658 /// admission rule strict-parses against DNS-1123-label, the Helm
3659 /// operator's tracking-secret name is derived from `release_name`
3660 /// and is itself DNS-1123-label-bounded, and the rendered chart's
3661 /// K8s object `metadata.name` axes embed the chart name as a
3662 /// prefix — every one fails admission on a > 63-byte chart name.
3663 ///
3664 /// The per-axis [`Self::validate_nome`] gate (6c992f8) already
3665 /// caps `:nome` itself at 63 bytes via [`is_dns_1123_label`], so a
3666 /// `:nome` of 56–63 bytes silently passed validate (the inner
3667 /// DNS-1123 check accepts the bare `:nome`) but produced a
3668 /// `lareira-<nome>` of 64–71 bytes that the apiserver / `helm lint`
3669 /// rejected at admission — far from the source `caixa.lisp`, with
3670 /// no field naming the overflow root cause. The
3671 /// [`lareira_chart_name`] helper's own doc comment
3672 /// (caixa-core/src/render.rs:3198) explicitly deferred the fix:
3673 /// "the M4 admission webhook will pin the joint-length invariant
3674 /// when it lands". This gate lands the invariant at the
3675 /// manifest-validate layer rather than waiting for the apiserver
3676 /// — the same fail-at-the-source posture every peer per-axis
3677 /// value-shape gate (DNS-1123 on `:nome`, SemVer-2 on `:versao`,
3678 /// SPDX-expression-shape on `:licenca`, 4-digit decimal year on
3679 /// `:edicao`, etc.) takes.
3680 ///
3681 /// Thin wrapper around
3682 /// [`crate::render::is_lareira_chart_name_shape`] (the
3683 /// substrate-side predicate that composes [`lareira_chart_name`] +
3684 /// [`is_dns_1123_label`] via the lifted
3685 /// [`crate::LAREIRA_CHART_NAME_NOME_MAX_LEN`] budget); maps the
3686 /// shared parser-shaped reason into the
3687 /// [`ManifestError::NomeChartNameBudgetExceeded`] variant so the
3688 /// diagnostic is self-locating (the offending `:nome` is named
3689 /// verbatim alongside the rendered chart name and the budget) and
3690 /// the author can shorten in one edit. The gate runs across every
3691 /// `:kind` — `:nome` is the substrate-wide identity axis any
3692 /// future renderer the substrate adds can derive a
3693 /// `lareira-<nome>` artifact from, and uniform enforcement closes
3694 /// the drift footgun where a future kind grows a chart-emitting
3695 /// render path while the validate cascade doesn't catch it.
3696 ///
3697 /// Runs *after* [`Self::validate_nome`] so the narrower
3698 /// `NomeEmpty` / `NomeInvalid` shape diagnostics fire first — a
3699 /// structurally-malformed `:nome` (empty, uppercase, underscore,
3700 /// dot, leading/trailing hyphen, Unicode, > 63 bytes) surfaces its
3701 /// specific shape error rather than the chart-name-budget error,
3702 /// preserving the legitimate "well-shaped `:nome` that happens to
3703 /// overflow the joint cap" arm for this gate.
3704 pub fn validate_nome_chart_name_budget(&self) -> Result<(), ManifestError> {
3705 let nome = self.nome();
3706 crate::render::is_lareira_chart_name_shape(nome).map_err(|reason| {
3707 ManifestError::NomeChartNameBudgetExceeded {
3708 nome: nome.to_string(),
3709 reason,
3710 }
3711 })
3712 }
3713
3714 /// Reject `:versao` values that don't parse as [`semver::Version`].
3715 /// The top-level Caixa version flows directly into every
3716 /// substrate-side artifact that carries a "this is which version of
3717 /// the caixa" axis: the `lareira-<nome>` Helm chart's `Chart.yaml`
3718 /// `version:` + `appVersion:` axes ([`caixa-helm::lib`] —
3719 /// SemVer-2-strict at `helm template` / `helm install` time per
3720 /// https://helm.sh/docs/topics/charts/#charts-and-versioning), the
3721 /// `feira publish` Zig-style `v<versao>` git tag
3722 /// ([`caixa-flux::lib::programs_yaml_entry`] / the
3723 /// `caixa-publish.yml` reusable workflow), the programs.yaml entry's
3724 /// `versao:` value the `lareira-fleet-programs` aggregator carries
3725 /// onto each rendered ComputeUnit, the OCI image's `:v<versao>` /
3726 /// `:latest` tags the substrate's `wasi-service-flake` builds with
3727 /// `skopeo push`, the lacre closure's pinned versions
3728 /// ([`caixa-resolver`] keys `concrete_versao`), and the
3729 /// `:upgrade-from :from` references peers in this exact `versao`
3730 /// shape (`semver::Version`, not `VersionReq`). Each consumer
3731 /// expects a strict three-part `MAJOR.MINOR.PATCH` (optionally
3732 /// `-prerelease` and/or `+build`); a structurally invalid `:versao`
3733 /// (`"0.1"` — missing patch, the canonical "I shortened it" footgun;
3734 /// `"v0.1.0"` — the git-tag-shape-leaking-into-versao typo;
3735 /// `"latest"` / `"main"` — the "I confused it with a docker tag"
3736 /// footgun; `"^0.1"` / `"~0.1.2"` — the requirement-shape leaking
3737 /// into the version field a peer `:deps :versao` accepts;
3738 /// `"0.1.0.0"` — the four-part Java/Microsoft convention DNS
3739 /// SemVer-2 forbids) silently passed [`Caixa::from_lisp`] (the
3740 /// derive macro stores the raw String) and the failure surfaced at
3741 /// the *first* downstream consumer that strict-parses it: at
3742 /// `helm install` time as a chart-version rejection, at
3743 /// `feira publish` time as a malformed git tag, at lacre-resolve
3744 /// time as a `semver::Error` not naming the offending caixa, at
3745 /// `feira upgrade --to <versao>` time as an unresolvable
3746 /// `:upgrade-from :from` match — far from the source `caixa.lisp`
3747 /// and without any field naming the offending `:versao`.
3748 ///
3749 /// Thin wrapper around [`semver::Version::parse`] — the same parser
3750 /// [`crate::CaixaVersion::parse`] (the typed `:versao` accessor)
3751 /// and [`crate::UpgradeFromEntry::validate`] (the peer
3752 /// `:upgrade-from :from` axis, 26da2c7) consume. Maps the
3753 /// `semver::Error` reason into the [`ManifestError::VersaoInvalid`]
3754 /// variant, carrying the offending `:versao` verbatim + a
3755 /// parser-shaped reason naming the specific violation, so the
3756 /// diagnostic is self-locating (the author can grep their
3757 /// `caixa.lisp` for `:versao "<value>"` and fix it in one edit).
3758 /// Same diagnostic shape as [`ManifestError::NomeInvalid`]
3759 /// (6c992f8) and [`crate::UpgradeError::FromInvalid`]
3760 /// (b0c8389) on the peer axes. With this gate, the typed `:versao`
3761 /// surfaces — top-level `:versao`, `:upgrade-from :from` — are
3762 /// now structurally equivalent (every value past validate is
3763 /// round-trippable through [`semver::Version::parse`] without
3764 /// re-checking at the renderer, resolver, or operator hot-upgrade
3765 /// layer), peer with the four `:versao` requirement axes (`:deps`,
3766 /// `:deps-dev`, `:membros`, `:children`) the prior commits
3767 /// (2420c44, 9888b13, b38ff3a) wired through `parse_requirement`.
3768 ///
3769 /// Empty `:versao` (which [`Caixa::from_lisp`] does not reject —
3770 /// the derive macro stores the raw String) is gated by the
3771 /// narrower [`ManifestError::VersaoEmpty`] arm before the parser is
3772 /// consulted, mirroring the empty-first cascade every per-axis
3773 /// version gate already uses (e.g. `MembroVersaoEmpty` before
3774 /// `MembroVersaoInvalid`, `EmptyChildVersion` before
3775 /// `ChildVersaoInvalid`, `NomeEmpty` before `NomeInvalid`).
3776 pub fn validate_versao(&self) -> Result<(), ManifestError> {
3777 let versao = self.versao();
3778 if versao.is_empty() {
3779 return Err(ManifestError::VersaoEmpty);
3780 }
3781 semver::Version::parse(versao).map_err(|e| ManifestError::VersaoInvalid {
3782 versao: versao.to_string(),
3783 reason: e.to_string(),
3784 })?;
3785 Ok(())
3786 }
3787
3788 /// Reject `:restart-window` values the shared
3789 /// [`crate::supervisor::duration_codec::parse`] refuses. The flat
3790 /// `restart_window: Option<String>` slot on [`Caixa`] is stored
3791 /// raw by the derive macro (the typed [`SupervisorSpec`] holds an
3792 /// `Option<Duration>` routed through the shared codec via `with =
3793 /// "duration_codec"`); the inline `Caixa → SupervisorSpec`
3794 /// view-construction path ([`Self::supervisor_view`]) folds the
3795 /// raw string through the same shared codec and soft-swallows the
3796 /// parse error as `None` to keep the view best-effort. Without
3797 /// this gate a malformed `:restart-window` (`"1.5s"` — the
3798 /// fractional-seconds drift class; `"1.0s"` — the decimal-shaped
3799 /// integer drift; `"0.5m"` — the unit-fraction drift; `"+30s"` /
3800 /// `"-30s"` — the leading-sign drift; `"30x"` — the unknown-unit
3801 /// footgun; `"abc"` — pure garbage; `""` — the empty-after-trim
3802 /// edge case) silently produced a `SupervisorSpec` with
3803 /// `restart_window: None`, indistinguishable from the canonical
3804 /// "omit the slot to express no reset" authoring shape — Erlang/OTP's
3805 /// `MaxIntensity / Period` invariant turns into a never-reset
3806 /// supervisor far from the source `caixa.lisp`, with no field
3807 /// naming the offending `:restart-window`. Lifting the gate to a
3808 /// Caixa-level validator mirrors the trajectory of the peer
3809 /// per-axis identity gates ([`Self::validate_nome`] 6c992f8,
3810 /// [`Self::validate_versao`] 1fdaa02, [`Self::validate_deps`]
3811 /// a7f0d8c) and the ABSORPTION-ROADMAP.md M2.2 test pin
3812 /// (line 196: "reject invalid `:restart-window` (non-duration)").
3813 ///
3814 /// Thin wrapper around [`crate::supervisor::duration_codec::parse`]
3815 /// (the shared codec backing `:supervisor :restart-window` as
3816 /// serde-routed on [`SupervisorSpec`], `:politicas :timeout`, and
3817 /// `:politicas :circuit-breaker :window` — all three covered by
3818 /// the integer-magnitude gate 1c55a2a). Maps the codec's parse
3819 /// error verbatim into the [`ManifestError::RestartWindowMalformed`]
3820 /// variant, carrying the offending raw string + a parser-shaped
3821 /// reason naming the canonical authoring form, so the diagnostic
3822 /// is self-locating (the author can grep their `caixa.lisp` for
3823 /// `:restart-window "<value>"` and fix it in one edit) and
3824 /// uniform with every other manifest-level validate diagnostic.
3825 /// With this gate the four `:restart-window`-shaped surfaces (the
3826 /// flat raw string on [`Caixa`], the typed `Option<Duration>` on
3827 /// [`SupervisorSpec`], the two `MeshPolicy` peer durations) are
3828 /// now structurally equivalent — every value past the codec is in
3829 /// one accepted set, by construction.
3830 ///
3831 /// `None` (the canonical "omit the slot to express no reset"
3832 /// shape) is accepted trivially — the gate is a no-op when the
3833 /// author didn't author a window. The empty string is rejected by
3834 /// the shared codec (its digit-only gate refuses an empty
3835 /// magnitude), surfacing the same `RestartWindowMalformed`
3836 /// diagnostic as every other rejected non-canonical shape.
3837 pub fn validate_restart_window(&self) -> Result<(), ManifestError> {
3838 let Some(s) = self.restart_window() else {
3839 return Ok(());
3840 };
3841 crate::supervisor::duration_codec::parse(s)
3842 .map(|_| ())
3843 .map_err(|reason| ManifestError::RestartWindowMalformed {
3844 restart_window: s.to_string(),
3845 reason,
3846 })
3847 }
3848
3849 /// Reject per-entry values on the three Caixa-level code-surface
3850 /// path lists (`:bibliotecas`, `:exe`, `:servicos`) that the
3851 /// layout checker's `root.join(p)` sandbox would silently subvert.
3852 /// Same three structural footguns the peer
3853 /// [`BehaviorSpec::validate`] (b0c8389) and
3854 /// [`crate::UpgradeInstruction::validate`] `StateChange` arm
3855 /// (26da2c7) already close on the M2 `:behavior :on-*` and
3856 /// `:upgrade-from :state-change :script` axes, here lifted onto
3857 /// the three top-level code-path axes through the shared
3858 /// [`is_sandboxed_relative_path`] predicate:
3859 ///
3860 /// - empty entry (`(:bibliotecas (""))` / `(:exe (""))` /
3861 /// `(:servicos (""))`): `PathBuf::new()` round-trips through
3862 /// [`Path::join`] as the base itself — `root.join("")` ==
3863 /// `root`, so the existence check (`self.exists(&root)`)
3864 /// trivially passes (the project root exists), and the layout
3865 /// silently treats the project root as a biblioteca / exe /
3866 /// servico entry. The `:bibliotecas` loop then hands the root
3867 /// to `tatara_lisp::read` at `feira build` time as if the root
3868 /// directory itself were a Lisp source file — a parse error
3869 /// far from the source `caixa.lisp` with no field naming the
3870 /// offending entry.
3871 /// - absolute path (`(:bibliotecas ("/etc/passwd"))`):
3872 /// [`Path::join`] *replaces* the base when the right-hand side
3873 /// is absolute, so `root.join("/etc/passwd")` resolves to
3874 /// `"/etc/passwd"` and escapes the project sandbox entirely.
3875 /// The existence check then silently consults whatever the
3876 /// escaped path resolves to — for `:bibliotecas`, the layout
3877 /// has no `starts_with`-fence (only `:exe` is fenced under
3878 /// `exe/` and `:servicos` under `servicos/`), so an absolute
3879 /// `:bibliotecas` entry that happens to resolve on disk
3880 /// silently passes. For `:exe` / `:servicos` the fence catches
3881 /// the absolute case downstream as `ExeOutsideDir` /
3882 /// `ServicoOutsideDir` (or `MissingEntry` if the absolute path
3883 /// doesn't exist), but with a downstream-shaped diagnostic
3884 /// that names the resolved escape path rather than the
3885 /// authoring footgun at the source.
3886 /// - parent-escape (`(:bibliotecas ("../sibling/x.lisp"))` /
3887 /// `(:exe ("exe/../../escape.lisp"))`): a [`PathBuf`] with any
3888 /// [`std::path::Component::ParentDir`] anywhere round-trips
3889 /// through [`Path::join`] as a traversal above the caixa root.
3890 /// The `:exe` / `:servicos` `starts_with(<dir>)` fence is
3891 /// *component-aware* (not canonical-path-aware), so
3892 /// `root.join("exe/../../escape.lisp")` `starts_with(exe_dir)`
3893 /// is **true** even though the canonical resolution
3894 /// `{parent of root}/escape.lisp` lives outside the caixa root
3895 /// — the fence silently lets the parent-escape through, and
3896 /// the existence check passes if that escape-target happens
3897 /// to exist. Caught regardless of where the `..` sits
3898 /// (leading, mid-path, trailing) so the gate matches the peer
3899 /// predicate's full coverage.
3900 ///
3901 /// Same `Empty` → `Absolute` → `ParentEscape` arm-ordering every peer
3902 /// `is_sandboxed_relative_path` consumer follows (b0c8389 / 26da2c7);
3903 /// same per-slot diagnostic shape every peer per-axis path-gate
3904 /// exposes (`*Empty { slot }` / `*Absolute { slot, path }` /
3905 /// `*ParentEscape { slot, path }`). Cross-slot precedence is
3906 /// `:bibliotecas` → `:exe` → `:servicos` — the same declaration
3907 /// order [`Caixa::declared_foreign_code_slots`] uses for its
3908 /// canonical foreign-code-slot diagnostic, so a manifest with
3909 /// multiple malformed slots surfaces the lexicographically-earliest
3910 /// slot's diagnostic deterministically.
3911 ///
3912 /// Lifted to the typed surface as a Caixa-level validator (peer
3913 /// of [`Self::validate_nome`] / [`Self::validate_versao`] /
3914 /// [`Self::validate_deps`] / [`Self::validate_restart_window`])
3915 /// and wired into [`crate::StandardLayout::verify`] before the
3916 /// existence-check loops so the diagnostic names the offending
3917 /// slot at the source caixa.lisp rather than reporting a
3918 /// downstream `MissingEntry` / `ExeOutsideDir` /
3919 /// `ServicoOutsideDir` against the resolved sandbox-escape path.
3920 /// The fourth typed code-path surface — every author-supplied
3921 /// path on the manifest — is now structurally accept-shaped
3922 /// past validate, peer with `:behavior :on-*` and
3923 /// `:upgrade-from :state-change :script`.
3924 pub fn validate_code_paths(&self) -> Result<(), ManifestError> {
3925 /// Per-slot file-type contract for the three Caixa-level
3926 /// code-path surfaces (`:bibliotecas`, `:exe`, `:servicos`).
3927 /// Each variant names the predicate the per-entry file-type
3928 /// gate consults; [`Self::None`] opts the slot out of any
3929 /// file-type contract. Lifted as a typed local enum so the
3930 /// per-slot dispatch is exhaustive at the `match` — adding a
3931 /// future axis to the typed-substrate `:` slot set (the
3932 /// future `:assets` resource axis the M5 roadmap names, the
3933 /// future `:nix-flake` derivation axis the caixa-flake
3934 /// emitter consults) lands as one variant + one `match` arm,
3935 /// not a coordinated rewrite of every per-slot bool flag.
3936 ///
3937 /// Peer of the typed-substrate per-slot variant disciplines
3938 /// already established on this surface
3939 /// ([`crate::supervisor::RestartStrategy`] +
3940 /// [`crate::supervisor::RestartPolicy`] on the OTP-shape
3941 /// supervision-tree axis,
3942 /// [`crate::aplicacao::PlacementStrategy`] on the §III.1
3943 /// placement axis, [`crate::aplicacao::WitTarget`] on the
3944 /// `:contratos` payload-target axis): the typed `enum` is
3945 /// the substrate's single source of truth for the per-axis
3946 /// dispatch, and every consumer (the per-arm body here, the
3947 /// future feira-lint per-slot diagnostic renderer, the M4
3948 /// per-axis admission webhook) reaches for the same typed
3949 /// surface rather than re-deriving the partition from inline
3950 /// flag combinations.
3951 enum CodePathFileType {
3952 /// `:exe` — nix-build derivation output, no terminating-
3953 /// extension contract (the canonical `"exe/<name>"`
3954 /// fixtures the layout's `ExeOutsideDir` error message
3955 /// documents carry no extension by convention).
3956 None,
3957 /// `:bibliotecas` — tatara-lisp source files the
3958 /// `feira build` loop reads through `tatara_lisp::read`
3959 /// at parse time. Routes to [`is_lisp_extension`].
3960 LispSource,
3961 /// `:servicos` — ComputeUnit-CR YAML files the
3962 /// caixa-helm / caixa-flux renderers consume through
3963 /// `serde_yaml::from_str`. Routes to
3964 /// [`is_computeunit_yaml_extension`].
3965 ComputeUnitYaml,
3966 }
3967
3968 // The per-slot [`CodePathFileType`] selects which axes carry the
3969 // lifted file-type predicate. `:bibliotecas` is the tatara-lisp
3970 // source axis (the `feira build` loop at
3971 // `caixa-feira/src/cmd/build.rs:33` reads each entry through
3972 // `tatara_lisp::read` at parse time) — the lifted
3973 // [`is_lisp_extension`] predicate gates the `.lisp` extension.
3974 // `:exe` is the nix-built executable surface (per the canonical
3975 // `"exe/<name>"`-shaped fixtures the layout's `ExeOutsideDir`
3976 // error message documents and every in-tree
3977 // `caixa_with_code_paths` positive control uses) — its file-type
3978 // contract is "nix-build derivation output", not a typed source
3979 // file, so [`CodePathFileType::None`] opts the slot out of any
3980 // file-type gate. `:servicos` is the `.computeunit.yaml`
3981 // ComputeUnit-CR axis (the peer caixa-helm / caixa-flux
3982 // renderers consume each entry through `serde_yaml::from_str` as
3983 // a typed `ComputeUnit` CR) — the lifted
3984 // [`is_computeunit_yaml_extension`] predicate gates the compound
3985 // `.computeunit.yaml` suffix. All three axes are surfaced through
3986 // the same iteration so the sandbox-shape + duplicate gates
3987 // apply uniformly; the typed file-type dispatch fires per-slot
3988 // exactly where the downstream consumer's accepted set demands
3989 // it. The third file-type variant ([`ComputeUnitYaml`]) is the
3990 // compounding lift on the peer 64772a9 `:bibliotecas`
3991 // `.lisp`-gate trajectory — the second of the three code-path
3992 // axes to land on a typed compound-suffix gate, with the same
3993 // self-locating per-slot diagnostic shape every peer per-axis
3994 // file-type lift uses (`*NonLispExtension { slot, path }` /
3995 // `*NonComputeUnitYamlExtension { slot, path }`).
3996 for (slot, list, file_type) in [
3997 (
3998 ":bibliotecas",
3999 &self.bibliotecas,
4000 CodePathFileType::LispSource,
4001 ),
4002 (":exe", &self.exe, CodePathFileType::None),
4003 (
4004 ":servicos",
4005 &self.servicos,
4006 CodePathFileType::ComputeUnitYaml,
4007 ),
4008 ] {
4009 // Per-slot set-not-multiset gate on the typed code-path axis.
4010 // Every peer Vec-shaped author-supplied list past validate is
4011 // a set, not a multiset: `:membros :caixa`
4012 // ([`crate::AplicacaoError::MembroDuplicate`]), `:placement
4013 // :clusters` ([`crate::AplicacaoError::PlacementClusterDuplicate`]),
4014 // `:entrada :paths` ([`crate::AplicacaoError::EntradaPathDuplicate`]),
4015 // `:contratos` ([`crate::AplicacaoError::ContratoDuplicate`]),
4016 // `:children :caixa` ([`crate::SupervisorError::DuplicateChild`]),
4017 // `:deps` / `:deps-dev` `:nome` ([`crate::DepError::DuplicateNome`]
4018 // per 359fba5), `:upgrade-from :from` ([`crate::UpgradeError::DuplicateFrom`]),
4019 // `:etiquetas` ([`ManifestError::EtiquetaDuplicate`] per 360a499),
4020 // `:autores` ([`ManifestError::AutorDuplicate`] per 86c769b) —
4021 // the three code-path lists are the last Vec-shaped author-
4022 // supplied slots on the typed Caixa surface still admitting a
4023 // duplicate entry silently. Scope is per-list (`:bibliotecas`
4024 // duplicates are flagged within `:bibliotecas`, not across
4025 // `:bibliotecas` ↔ `:exe`) — the same per-list scope `:deps`
4026 // ↔ `:deps-dev` use (a `:nome` present in both lists is a
4027 // legitimate dev-vs-runtime shape on the dep axis, fenced
4028 // separately by [`crate::dep::validate_no_self_dep`]). On the
4029 // code-path axis a cross-slot collision is structurally
4030 // impossible by the layout's `starts_with(<exe|servicos>_dir)`
4031 // fence — `:exe` and `:servicos` entries are confined to their
4032 // own directory trees, so the only way a string could appear
4033 // on two code-path lists is the (rare, structurally invalid)
4034 // case where `:bibliotecas` carries an `"exe/<x>"` or
4035 // `"servicos/<x>.yaml"`-shaped path.
4036 //
4037 // Without the gate three authoring footguns silently passed:
4038 //
4039 // - `:bibliotecas ("lib/foo.lisp" "lib/foo.lisp")` — the
4040 // canonical copy-paste-the-wrong-file footgun. `feira
4041 // build` (`caixa-feira/src/cmd/build.rs:33`) walks the
4042 // list and re-parses the same file twice, wasting work
4043 // and silently masking the author's intent to declare a
4044 // *second* biblioteca.
4045 // - `:exe ("exe/cli" "exe/cli")` — the same footgun on the
4046 // Binario surface. The future `caixa-flake` `nix flake`
4047 // emitter that materializes each `:exe` entry as a flake
4048 // `packages.<exe-name>` derivation would collide on the
4049 // duplicate package name and surface a flake-eval error
4050 // far from the source `caixa.lisp`.
4051 // - `:servicos ("servicos/x.computeunit.yaml"
4052 // "servicos/x.computeunit.yaml")` — the same footgun on
4053 // the Servico surface. The peer `caixa-helm` / `caixa-flux`
4054 // renderers already refuse `:servicos.len() != 1` with
4055 // the narrower [`UnsupportedServicoCount`] diagnostic, but
4056 // that diagnostic surfaces "too many servicos" without
4057 // naming "duplicate entry" — the typed self-locating
4058 // "which entry is the duplicate" framing only lands at
4059 // this gate.
4060 //
4061 // Same `seen.insert(entry.as_str())` shape every peer per-list
4062 // duplicate gate uses (`:etiquetas` 360a499, `:autores`
4063 // 86c769b, `:deps` 359fba5) and the same "structural shape
4064 // checks fire before the duplicate check on the same entry"
4065 // ordering (a `(:bibliotecas ("" "lib/x.lisp" "lib/x.lisp"))`
4066 // shape surfaces the narrower [`Self::CodePathEmpty`] for the
4067 // empty entry first, not the duplicate on the later pair).
4068 let mut seen = std::collections::HashSet::new();
4069 for entry in list {
4070 let path = Path::new(entry);
4071 match is_sandboxed_relative_path(path) {
4072 Ok(()) => {}
4073 Err(PathShapeViolation::Empty) => {
4074 return Err(ManifestError::CodePathEmpty { slot });
4075 }
4076 Err(PathShapeViolation::Absolute) => {
4077 return Err(ManifestError::CodePathAbsolute {
4078 slot,
4079 path: path.to_path_buf(),
4080 });
4081 }
4082 Err(PathShapeViolation::ParentEscape) => {
4083 return Err(ManifestError::CodePathParentEscape {
4084 slot,
4085 path: path.to_path_buf(),
4086 });
4087 }
4088 }
4089 // The per-slot file-type gate dispatched through the
4090 // typed [`CodePathFileType`] selector above. Each variant
4091 // routes to the lifted predicate the downstream consumer
4092 // demands:
4093 //
4094 // - [`LispSource`] → [`is_lisp_extension`] for
4095 // `:bibliotecas` (the `feira build` loop's
4096 // `tatara_lisp::read` consumer);
4097 // - [`ComputeUnitYaml`] → [`is_computeunit_yaml_extension`]
4098 // for `:servicos` (the caixa-helm / caixa-flux
4099 // `serde_yaml::from_str` consumer's `ComputeUnit` CR
4100 // accepted set);
4101 // - [`None`] for `:exe` — the nix-build derivation-
4102 // output axis has no terminating-extension contract.
4103 //
4104 // Fires after the sandbox-shape arms so a path that is
4105 // *both* sandbox-escaping and wrong-extension surfaces
4106 // the more fundamental sandbox-shape diagnostic first
4107 // (mirrors the peer `EmptyPath` → `AbsolutePath` →
4108 // `ParentEscape` → `NonLispExtension` arm-ordering on
4109 // `:behavior :on-*` c97815a, and `EmptyScript` →
4110 // `AbsoluteScript` → `ParentEscapeScript` →
4111 // `NonLispExtensionScript` on
4112 // `:upgrade-from :state-change :script` 33cc830), and
4113 // before the duplicate gate so the narrower per-entry
4114 // file-type shape dominates the cross-entry uniqueness
4115 // diagnostic (a
4116 // `("servicos/x.yaml" "servicos/x.yaml")` shape on
4117 // `:servicos` surfaces
4118 // `CodePathNonComputeUnitYamlExtension` on the first
4119 // entry rather than `CodePathDuplicate` on the pair —
4120 // peer with the 64772a9 `:bibliotecas`
4121 // `("lib/x.txt" "lib/x.txt")` ordering).
4122 match file_type {
4123 CodePathFileType::None => {}
4124 CodePathFileType::LispSource => {
4125 if !is_lisp_extension(path) {
4126 return Err(ManifestError::CodePathNonLispExtension {
4127 slot,
4128 path: path.to_path_buf(),
4129 });
4130 }
4131 }
4132 CodePathFileType::ComputeUnitYaml => {
4133 if !is_computeunit_yaml_extension(path) {
4134 return Err(ManifestError::CodePathNonComputeUnitYamlExtension {
4135 slot,
4136 path: path.to_path_buf(),
4137 });
4138 }
4139 }
4140 }
4141 crate::render::insert_first_seen(&mut seen, entry.as_str(), || {
4142 ManifestError::CodePathDuplicate {
4143 slot,
4144 path: path.to_path_buf(),
4145 }
4146 })?;
4147 }
4148 }
4149 Ok(())
4150 }
4151
4152 /// Reject `:etiquetas` lists with an empty entry or with two entries
4153 /// agreeing on the same string. `:etiquetas` is the universal
4154 /// registry-search-tag axis on [`Caixa`] (every kind carries the
4155 /// `Vec<String>` slot) and lands verbatim as the Helm chart
4156 /// `Chart.yaml` `keywords:` array on every Servico (caixa-helm's
4157 /// `build_chart_yaml` at `caixa-helm/src/lib.rs:236` folds it through
4158 /// a [`std::collections::BTreeSet`] alongside the four substrate-
4159 /// fixed tags `lareira` / `wasm` / `tatara-lisp` / `caixa-servico`).
4160 /// Two authoring footguns silently passed validate without this gate:
4161 ///
4162 /// - Empty entry (`(:etiquetas (""))` — the canonical paste-from-
4163 /// blank-doc footgun) rendered as `keywords: ["", "caixa-servico",
4164 /// "lareira", "tatara-lisp", "wasm"]` in `Chart.yaml`. Helm's
4165 /// `chart.metadata.keywords` admits the value without a strict
4166 /// parser-side gate, but the empty keyword has no operational
4167 /// meaning — it indexes nothing in the future caixa-registry
4168 /// search axis and clutters the rendered chart with a no-op tag.
4169 /// - Duplicate entries (`(:etiquetas ("demo" "demo"))` — the
4170 /// copy-paste-the-wrong-tag footgun) silently passed validate
4171 /// and were silently dedup'd by caixa-helm's `BTreeSet` collect
4172 /// at chart render — a "second wins / one silently disappears"
4173 /// shape divergent from every peer typed-graph set gate
4174 /// ([`crate::AplicacaoError::MembroDuplicate`] on `:membros`,
4175 /// [`crate::AplicacaoError::PlacementClusterDuplicate`] on
4176 /// `:placement :clusters`, [`crate::AplicacaoError::EntradaPathDuplicate`]
4177 /// on `:entrada :paths`, [`crate::AplicacaoError::ContratoDuplicate`]
4178 /// on `:contratos`, [`crate::DepError::DuplicateNome`] on
4179 /// `:deps` / `:deps-dev` per 359fba5, [`crate::UpgradeError::DuplicateFrom`]
4180 /// on `:upgrade-from`, the per-instruction-class singularity
4181 /// gates [`crate::UpgradeError::DuplicateLoadModule`] /
4182 /// [`crate::UpgradeError::DuplicateStateChange`] /
4183 /// [`crate::UpgradeError::DuplicateCleanup`]). The typed-graph
4184 /// discipline is uniform: every Vec-shaped author-supplied list
4185 /// past validate is set-not-multiset, by construction.
4186 ///
4187 /// Past the empty arm the gate enforces the chart-keyword shape
4188 /// predicate via [`crate::render::is_chart_keyword_shape`]: Cargo's
4189 /// crates.io `[package] keywords` grammar — 1..=20 bytes, starts
4190 /// with an ASCII letter, ASCII alphanumeric / `_` / `-`
4191 /// continuation. Closes the canonical paste-from-doc footguns the
4192 /// bare empty + duplicate arms left open: paste-from-aligned-doc
4193 /// whitespace (`" mesh"`, `"mesh "`), paste-from-multiline-doc
4194 /// newline (`"mesh\nhttp"` — the author pasted a multi-tag block
4195 /// into one entry instead of splitting), paste-from-Windows-CRLF-doc
4196 /// carriage return, CSV-list-separator confusion (`"mesh,http,grpc"`
4197 /// — the author meant three separate list entries), path-separator
4198 /// confusion (`"caixa/servico"`), namespace-suffix (`"http.1"`),
4199 /// leading-digit (`"1foo"`), kebab-leak (`"-foo"`), snake-leak
4200 /// (`"_foo"`), non-ASCII (`"café"`), and paste-from-binary-blob
4201 /// control bytes that would silently land as malformed search tags
4202 /// in the rendered Chart.yaml `keywords:` array and break the
4203 /// Artifact Hub keyword index lookup far from the source caixa.lisp.
4204 /// Mirrors the [`Self::validate_autores`] shape-predicate cascade
4205 /// established on the sibling universal-axis `Vec<String>` surface
4206 /// — the second universal-axis Vec<String> surface to land the
4207 /// empty-first-then-shape-then-duplicate per-entry cascade.
4208 ///
4209 /// Same empty-first cascade discipline every peer per-axis gate
4210 /// uses: the per-entry empty arm fires before the per-entry shape
4211 /// arm fires before the cross-entry duplicate arm, so an
4212 /// `("" "mesh" "mesh")` authoring shape surfaces the narrower
4213 /// [`ManifestError::EtiquetaEmpty`] (the structural "this entry
4214 /// has no value" defect) before either the shape or the duplicate
4215 /// diagnostic. Walks the list in declaration order so the
4216 /// first-collision diagnostic surfaces the lexicographically-
4217 /// earliest offending position, peer with every other duplicate
4218 /// gate on this surface.
4219 ///
4220 /// Universal-axis (every kind carries `:etiquetas`), so wired at the
4221 /// caixa-build gate alongside the peer universal gates
4222 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4223 /// [`Self::validate_deps`] / [`Self::validate_code_paths`] — before
4224 /// the kind-coherence gates ([`crate::LayoutError::MeshSlotsOnNonAplicacao`]
4225 /// / [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4226 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4227 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-specific
4228 /// slot sets. The future caixa-registry search axis can reach for
4229 /// `caixa.etiquetas` knowing every entry is a non-empty distinct
4230 /// chart-keyword-shaped string without re-deriving the precondition.
4231 pub fn validate_etiquetas(&self) -> Result<(), ManifestError> {
4232 let mut seen = std::collections::HashSet::new();
4233 for etiqueta in self.etiquetas() {
4234 if etiqueta.is_empty() {
4235 return Err(ManifestError::EtiquetaEmpty);
4236 }
4237 crate::render::is_chart_keyword_shape(etiqueta).map_err(|reason| {
4238 ManifestError::EtiquetaInvalid {
4239 etiqueta: etiqueta.clone(),
4240 reason,
4241 }
4242 })?;
4243 crate::render::insert_first_seen(&mut seen, etiqueta.as_str(), || {
4244 ManifestError::EtiquetaDuplicate {
4245 etiqueta: etiqueta.clone(),
4246 }
4247 })?;
4248 }
4249 Ok(())
4250 }
4251
4252 /// Reject `:autores` lists with an empty entry or with two entries
4253 /// agreeing on the same string. `:autores` is the universal
4254 /// maintainer-axis on [`Caixa`] (every kind carries the
4255 /// `Vec<String>` slot) and lands verbatim as the Helm chart
4256 /// `Chart.yaml` `maintainers:` array on every Servico (caixa-helm's
4257 /// `build_chart_yaml` at `caixa-helm/src/lib.rs:251` maps each entry
4258 /// to a `Maintainer { name, email: None }` without dedup). Two
4259 /// authoring footguns silently passed validate without this gate:
4260 ///
4261 /// - Empty entry (`(:autores (""))` — the canonical paste-from-
4262 /// blank-doc footgun) rendered as
4263 /// `maintainers: [{name: "", email: null}]` in `Chart.yaml`. The
4264 /// empty maintainer name has no operational meaning — it
4265 /// identifies no one in the substrate's authorship index and
4266 /// clutters the rendered chart with a no-op maintainer.
4267 /// - Duplicate entries (`(:autores ("pleme-io" "pleme-io"))` —
4268 /// the copy-paste-the-wrong-author footgun) silently passed
4269 /// validate and rendered as two identical maintainer entries.
4270 /// Unlike the [`Self::validate_etiquetas`] peer (caixa-helm's
4271 /// `BTreeSet`-collect on `:etiquetas` silently dedups the
4272 /// rendered `keywords:` array at chart-render time), the
4273 /// `maintainers:` rendering has *no* dedup — duplicate `:autores`
4274 /// entries stack verbatim in the chart, divergent from every
4275 /// peer typed-graph set gate ([`crate::AplicacaoError::MembroDuplicate`]
4276 /// on `:membros`, [`crate::AplicacaoError::PlacementClusterDuplicate`]
4277 /// on `:placement :clusters`, [`crate::AplicacaoError::EntradaPathDuplicate`]
4278 /// on `:entrada :paths`, [`crate::AplicacaoError::ContratoDuplicate`]
4279 /// on `:contratos`, [`crate::DepError::DuplicateNome`] on
4280 /// `:deps` / `:deps-dev`, [`crate::UpgradeError::DuplicateFrom`]
4281 /// on `:upgrade-from`, [`ManifestError::EtiquetaDuplicate`] on
4282 /// `:etiquetas`).
4283 ///
4284 /// Past the empty arm the gate enforces the chart-maintainer-name
4285 /// shape predicate via [`crate::render::is_chart_maintainer_name_shape`]:
4286 /// the structural single-line printable-UTF-8 floor every realistic
4287 /// Helm chart maintainer name carries — 1..=128 bytes, no leading
4288 /// or trailing whitespace, no ASCII control characters anywhere,
4289 /// Unicode bytes accepted. Closes the canonical paste-from-doc
4290 /// footguns the bare empty + duplicate arms left open:
4291 /// paste-from-aligned-doc whitespace (`" pleme-io"`, `"pleme-io "`),
4292 /// paste-from-multiline-doc newline (`"alice\nbob"` — the author
4293 /// pasted a multi-line block of author records into one `:autores`
4294 /// entry instead of splitting into one entry per author),
4295 /// paste-from-Windows-CRLF-doc carriage return, tab-from-aligned-doc,
4296 /// and the paste-from-binary-blob control bytes that would silently
4297 /// land as YAML-illegal byte sequences in the rendered Chart.yaml
4298 /// `maintainers:` array. Mirrors the shape-predicate cascade
4299 /// [`Self::validate_descricao`] / [`Self::validate_licenca`] /
4300 /// [`Self::validate_edicao`] / [`Self::validate_repositorio`]
4301 /// establish past their own empty arms on the sibling universal-axis
4302 /// `Option<String>` surfaces — the first universal-axis Vec<String>
4303 /// surface to land the empty-first-then-shape-then-duplicate per-entry
4304 /// cascade.
4305 ///
4306 /// Same empty-first cascade discipline every peer per-axis gate
4307 /// uses: the per-entry empty arm fires before the per-entry shape
4308 /// arm before the cross-entry duplicate arm. Walks the list in
4309 /// declaration order so the first-collision diagnostic surfaces the
4310 /// lexicographically-earliest offending position, peer with every
4311 /// other duplicate gate on this surface.
4312 ///
4313 /// Universal-axis (every kind carries `:autores`), so wired at the
4314 /// caixa-build gate alongside the peer universal gates
4315 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4316 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
4317 /// [`Self::validate_code_paths`] — before the kind-coherence gates
4318 /// ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4319 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4320 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4321 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-specific
4322 /// slot sets.
4323 pub fn validate_autores(&self) -> Result<(), ManifestError> {
4324 let mut seen = std::collections::HashSet::new();
4325 for autor in self.autores() {
4326 if autor.is_empty() {
4327 return Err(ManifestError::AutorEmpty);
4328 }
4329 crate::render::is_chart_maintainer_name_shape(autor).map_err(|reason| {
4330 ManifestError::AutorInvalid {
4331 autor: autor.clone(),
4332 reason,
4333 }
4334 })?;
4335 crate::render::insert_first_seen(&mut seen, autor.as_str(), || {
4336 ManifestError::AutorDuplicate {
4337 autor: autor.clone(),
4338 }
4339 })?;
4340 }
4341 Ok(())
4342 }
4343
4344 /// Reject `:repositorio` values whose shape the shared
4345 /// [`crate::render::is_git_repo_url`] predicate refuses. The flat
4346 /// `repositorio: Option<String>` slot on [`Caixa`] is the
4347 /// universal git-shaped homepage axis every kind carries — the
4348 /// substrate routes the same string through two load-bearing
4349 /// consumers:
4350 ///
4351 /// - [`caixa-helm`] folds it verbatim into the rendered
4352 /// `lareira-<nome>` Helm chart's `Chart.yaml` `home:` field
4353 /// (`build_chart_yaml` at `caixa-helm/src/lib.rs:268`) and into
4354 /// the chart `README.md` `repo = …` interpolation
4355 /// (`caixa-helm/src/lib.rs:359`).
4356 /// - [`caixa-flux`] folds it verbatim into the standalone
4357 /// `ClusterBundleOpts::for_caixa` `git_url:` field
4358 /// (`caixa-flux/src/lib.rs:293`), which becomes the `FluxCD`
4359 /// `GitRepository.spec.url` the cluster's source-controller
4360 /// polls — the load-bearing deploy-time axis.
4361 ///
4362 /// Both consumers use `Option::unwrap_or_else(|| <fallback>)` to
4363 /// substitute a placeholder when the slot is absent (`None` → the
4364 /// fallback fires); a `Some("")` *skips the fallback* and silently
4365 /// passes the empty string through to `Chart.yaml home: ""` /
4366 /// `GitRepository url: ""` — Helm's chart lint and `FluxCD`'s source
4367 /// controller both reject the empty URL far from the source
4368 /// `caixa.lisp`, with no field naming the offending `:repositorio`.
4369 /// Similarly a malformed `:repositorio` (whitespace, control char,
4370 /// missing `:` separator, leading `-`) silently lands in the
4371 /// rendered artifacts and breaks at `git clone` / `helm template`
4372 /// / `flux reconcile` time.
4373 ///
4374 /// Thin wrapper around [`crate::render::is_git_repo_url`] — the
4375 /// same shared predicate the peer [`crate::DepSource::validate`]
4376 /// routes the `:fonte (:tipo git :repo …)` axis through. With this
4377 /// gate the two `git URL`-shaped surfaces on the typed Caixa
4378 /// (`:repositorio` here, `:deps :fonte :repo` peer) are
4379 /// structurally equivalent: every value past validate is
4380 /// guaranteed-acceptable by the predicate's union of constraints
4381 /// (non-empty, length-bounded, no leading `-`, no whitespace, no
4382 /// control chars, ASCII only, no leading `:`, contains a `:`
4383 /// separator). The predicate accepts every documented authoring
4384 /// shape — `github:org/repo` shorthand, `https://host/path`,
4385 /// `ssh://[user@]host/path`, `git://host/path`, `git@host:path`
4386 /// scp-style SSH, `file:///path` — and refuses the canonical
4387 /// paste-from-blank-doc / paste-from-multiline-doc / CLI-arg-
4388 /// injection footguns at validate time. Maps the predicate's
4389 /// `String` reason verbatim into the
4390 /// [`ManifestError::RepositorioInvalid`] variant, carrying the
4391 /// offending value + parser-shaped reason so the diagnostic is
4392 /// self-locating (the author can grep their `caixa.lisp` for
4393 /// `:repositorio "<value>"` and fix it in one edit).
4394 ///
4395 /// `None` (the canonical "omit the slot to express no published
4396 /// homepage" shape) is accepted trivially — the gate is a no-op
4397 /// when the author didn't declare a value. `Some("")` is gated by
4398 /// the narrower [`ManifestError::RepositorioEmpty`] arm before the
4399 /// shape predicate is consulted, mirroring the empty-first cascade
4400 /// every peer per-axis identity gate uses
4401 /// ([`ManifestError::NomeEmpty`] → [`ManifestError::NomeInvalid`],
4402 /// [`ManifestError::VersaoEmpty`] → [`ManifestError::VersaoInvalid`],
4403 /// [`crate::DepError::FonteRepoEmpty`] →
4404 /// [`crate::DepError::FonteRepoInvalid`]).
4405 ///
4406 /// Universal-axis (every kind carries `:repositorio`), so wired at
4407 /// the caixa-build gate alongside the peer universal gates
4408 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4409 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
4410 /// [`Self::validate_autores`] / [`Self::validate_code_paths`] —
4411 /// before the kind-coherence gates
4412 /// ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4413 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4414 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4415 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-
4416 /// specific slot sets.
4417 pub fn validate_repositorio(&self) -> Result<(), ManifestError> {
4418 let Some(s) = self.repositorio() else {
4419 return Ok(());
4420 };
4421 if s.is_empty() {
4422 return Err(ManifestError::RepositorioEmpty);
4423 }
4424 is_git_repo_url(s).map_err(|reason| ManifestError::RepositorioInvalid {
4425 repositorio: s.to_string(),
4426 reason,
4427 })
4428 }
4429
4430 /// Reject `:descricao` values that are the empty string. The flat
4431 /// `descricao: Option<String>` slot on [`Caixa`] is the universal
4432 /// free-form-prose homepage axis every kind carries — the
4433 /// substrate routes the same string through two load-bearing
4434 /// consumers in the [`caixa-helm`] renderer:
4435 ///
4436 /// - `build_chart_yaml` folds it verbatim into the rendered
4437 /// `lareira-<nome>` Helm chart's `Chart.yaml` `description:`
4438 /// field (`caixa-helm/src/lib.rs:232-235`).
4439 /// - `build_readme` folds it verbatim into the rendered chart
4440 /// `README.md` header (`caixa-helm/src/lib.rs:333-336`).
4441 ///
4442 /// Both consumers use `Option::unwrap_or_else(|| <fallback>)` to
4443 /// substitute a `caixa.nome`-derived placeholder when the slot is
4444 /// absent (`None` → the fallback fires); a `Some("")` *skips the
4445 /// fallback* and silently passes the empty string through to
4446 /// `Chart.yaml description: ""` / a blank chart `README.md`
4447 /// header. Helm's chart spec requires a non-empty `description:`
4448 /// field on `apiVersion: v2` charts (`helm lint` surfaces it as
4449 /// `WARNING [chart.metadata.description]: description is required`),
4450 /// so the empty `Some("")` silently lands in the rendered
4451 /// artifacts and breaks at `helm lint` / `helm install` time far
4452 /// from the source `caixa.lisp`, with no field naming the
4453 /// offending `:descricao`.
4454 ///
4455 /// `None` (the canonical "omit the slot to defer to the renderer's
4456 /// `caixa.nome`-derived fallback" shape) is accepted trivially —
4457 /// the gate is a no-op when the author didn't declare a value.
4458 /// `Some("")` is gated by the narrower
4459 /// [`ManifestError::DescricaoEmpty`] arm, mirroring the empty-arm
4460 /// shape every peer per-axis empty gate uses
4461 /// ([`ManifestError::NomeEmpty`], [`ManifestError::VersaoEmpty`],
4462 /// [`ManifestError::EtiquetaEmpty`], [`ManifestError::AutorEmpty`],
4463 /// [`ManifestError::RepositorioEmpty`]).
4464 ///
4465 /// Universal-axis (every kind carries `:descricao`), so wired at
4466 /// the caixa-build gate alongside the peer universal gates
4467 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4468 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
4469 /// [`Self::validate_autores`] / [`Self::validate_repositorio`] /
4470 /// [`Self::validate_code_paths`] — before the kind-coherence
4471 /// gates ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4472 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4473 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4474 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-
4475 /// specific slot sets.
4476 ///
4477 /// Past the empty arm the gate enforces the chart-description
4478 /// shape predicate via [`crate::render::is_chart_description_shape`]:
4479 /// the structural single-line UTF-8 floor every realistic chart
4480 /// description in the wild matches — 1..=512 bytes, no leading
4481 /// or trailing whitespace, no ASCII control characters anywhere
4482 /// (`0x00..=0x1F` plus `0x7F` DEL — banning tab, newline,
4483 /// carriage return, and every other control byte), Unicode
4484 /// continuation bytes accepted (the canonical fixtures carry
4485 /// `→` and `—`). Closes the canonical paste-from-doc footguns
4486 /// the bare empty-arm gate left open: paste-from-aligned-doc
4487 /// leading / trailing whitespace (`" Checkout flow."`,
4488 /// `"Checkout flow. "`), paste-from-multiline-doc newline
4489 /// (`"Checkout\nflow."`), paste-from-Windows-CRLF-doc CR
4490 /// (`"Checkout\rflow."`), tab-from-aligned-doc
4491 /// (`"Checkout\tflow."`), and paste-from-binary-blob NUL / BEL /
4492 /// ESC / DEL bytes. Mirrors the shape-predicate cascade
4493 /// [`Self::validate_repositorio`] / [`Self::validate_licenca`] /
4494 /// [`Self::validate_edicao`] establish past their own empty arms
4495 /// on the sibling universal-axis `Option<String>` Caixa-level
4496 /// value-shape surfaces.
4497 ///
4498 /// The empty-first cascade discipline mirrors every peer per-axis
4499 /// identity gate: [`ManifestError::DescricaoEmpty`] runs before
4500 /// [`ManifestError::DescricaoInvalid`], so the narrower empty
4501 /// diagnostic surfaces on `Some("")` rather than the broader
4502 /// shape-predicate diagnostic — peer with how
4503 /// [`ManifestError::LicencaEmpty`] runs before
4504 /// [`ManifestError::LicencaInvalid`],
4505 /// [`ManifestError::EdicaoEmpty`] runs before
4506 /// [`ManifestError::EdicaoInvalid`],
4507 /// [`ManifestError::RepositorioEmpty`] runs before
4508 /// [`ManifestError::RepositorioInvalid`].
4509 pub fn validate_descricao(&self) -> Result<(), ManifestError> {
4510 let Some(s) = self.descricao() else {
4511 return Ok(());
4512 };
4513 if s.is_empty() {
4514 return Err(ManifestError::DescricaoEmpty);
4515 }
4516 crate::render::is_chart_description_shape(s).map_err(|reason| {
4517 ManifestError::DescricaoInvalid {
4518 descricao: s.to_string(),
4519 reason,
4520 }
4521 })?;
4522 Ok(())
4523 }
4524
4525 /// Reject `:licenca` values that are the empty string. The flat
4526 /// `licenca: Option<String>` slot on [`Caixa`] is the universal
4527 /// SPDX-shaped license-expression axis every kind carries — the
4528 /// substrate routes the same string through the [`caixa-helm`]
4529 /// renderer's `build_readme` which folds it verbatim into the
4530 /// rendered `lareira-<nome>` Helm chart's `README.md` `## License`
4531 /// section (`caixa-helm/src/lib.rs:361`) via
4532 /// `caixa.licenca.clone().unwrap_or_else(|| "MIT".into())`. The
4533 /// fallback only fires on `None`; a `Some("")` *skips the
4534 /// fallback* and silently passes the empty string through to a
4535 /// chart `README.md` whose `License` section renders as the bare
4536 /// trailing period (`.\n`) — peer footgun with the
4537 /// `Some("")`-skips-`unwrap_or_else` shape the
4538 /// [`Self::validate_descricao`] and [`Self::validate_repositorio`]
4539 /// gates close on the sibling free-form-prose and git-URL axes.
4540 ///
4541 /// `None` (the canonical "omit the slot to defer to the
4542 /// renderer's `MIT` fallback" shape every existing fixture
4543 /// carries) is accepted trivially — the gate is a no-op when the
4544 /// author didn't declare a value. `Some("")` is gated by the
4545 /// narrower [`ManifestError::LicencaEmpty`] arm, mirroring the
4546 /// empty-arm shape every peer per-axis empty gate uses
4547 /// ([`ManifestError::NomeEmpty`], [`ManifestError::VersaoEmpty`],
4548 /// [`ManifestError::EtiquetaEmpty`], [`ManifestError::AutorEmpty`],
4549 /// [`ManifestError::RepositorioEmpty`],
4550 /// [`ManifestError::DescricaoEmpty`]).
4551 ///
4552 /// Universal-axis (every kind carries `:licenca`), so wired at
4553 /// the caixa-build gate alongside the peer universal gates
4554 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4555 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
4556 /// [`Self::validate_autores`] / [`Self::validate_repositorio`] /
4557 /// [`Self::validate_descricao`] / [`Self::validate_code_paths`]
4558 /// — before the kind-coherence gates
4559 /// ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4560 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4561 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4562 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-
4563 /// specific slot sets.
4564 ///
4565 /// Past the empty arm the gate enforces the SPDX-expression shape
4566 /// predicate via [`crate::render::is_spdx_expression_shape`]: the
4567 /// structural alphabet floor every realistic SPDX expression in
4568 /// the wild uses — ASCII alphanumeric plus `.`, `-`, `+`, `(`,
4569 /// `)`, `:` (the `DocumentRef-…:LicenseRef-…` separator), and a
4570 /// single ASCII space (token separator). Closes the canonical
4571 /// paste-from-doc footguns the bare empty-arm gate left open:
4572 /// paste-from-doc whitespace (`"MIT "`, `" MIT"`), paste-from-
4573 /// multiline-doc CRLF (`"MIT\n"`), tab-from-aligned-doc
4574 /// (`"MIT\tOR Apache-2.0"`), non-ASCII smart-quote paste,
4575 /// underscore-instead-of-hyphen typo (`"Apache_2.0"`),
4576 /// comma-instead-of-`OR`-keyword colloquial idiom (`"MIT,
4577 /// Apache-2.0"`), slash-dual-license colloquial idiom (`"MIT/
4578 /// Apache-2.0"`), and semicolon-list-separator confusion
4579 /// (`"MIT; Apache-2.0"`). Mirrors the shape-predicate cascade
4580 /// [`Self::validate_repositorio`] / [`Self::validate_edicao`]
4581 /// establish past their own empty arms.
4582 ///
4583 /// The empty-first cascade discipline mirrors every peer per-axis
4584 /// identity gate: [`ManifestError::LicencaEmpty`] runs before
4585 /// [`ManifestError::LicencaInvalid`], so the narrower empty
4586 /// diagnostic surfaces on `Some("")` rather than the broader
4587 /// shape-predicate diagnostic — peer with how
4588 /// [`ManifestError::EdicaoEmpty`] runs before
4589 /// [`ManifestError::EdicaoInvalid`],
4590 /// [`ManifestError::RepositorioEmpty`] runs before
4591 /// [`ManifestError::RepositorioInvalid`].
4592 ///
4593 /// A future tightening on this axis can extend the alphabet
4594 /// floor into a full SPDX expression parser + license-id
4595 /// allowlist (rejecting alphabet-valid values that don't name a
4596 /// real SPDX license identifier — e.g., `"NotAReal"` is
4597 /// alphabet-valid but no `NotAReal` license-id exists). That
4598 /// parser only becomes meaningful past a real SPDX-spec
4599 /// dependency; this gate establishes the structural floor by
4600 /// refusing every non-SPDX-alphabet value at validate time.
4601 pub fn validate_licenca(&self) -> Result<(), ManifestError> {
4602 let Some(s) = self.licenca() else {
4603 return Ok(());
4604 };
4605 if s.is_empty() {
4606 return Err(ManifestError::LicencaEmpty);
4607 }
4608 crate::render::is_spdx_expression_shape(s).map_err(|reason| {
4609 ManifestError::LicencaInvalid {
4610 licenca: s.to_string(),
4611 reason,
4612 }
4613 })?;
4614 Ok(())
4615 }
4616
4617 /// Reject `:edicao` values that are the empty string. The flat
4618 /// `edicao: Option<String>` slot on [`Caixa`] is the universal
4619 /// language-edition axis every kind carries — it determines the
4620 /// tatara-lisp macro surface + compatibility flags the substrate
4621 /// applies when building a caixa, and lands verbatim in the
4622 /// `Caixa::template` author-time scaffold (the canonical
4623 /// `:edicao "2026"` line every `feira init` emits via
4624 /// [`Caixa::template`] at `caixa-core/src/manifest.rs:1193`) and
4625 /// in every renderer-side fixture (`caixa-helm/src/lib.rs:375`,
4626 /// `caixa-flux/src/lib.rs:445`, `caixa-mesh/src/lib.rs:629`,
4627 /// `caixa-core/src/render.rs:2510`) via
4628 /// `edicao: Some("2026".into())`.
4629 ///
4630 /// `None` (the canonical "omit the slot to defer to the
4631 /// substrate's default edition" shape every existing
4632 /// [`caixa-resolver`] integration test fixture carries via
4633 /// `edicao: None` — see `caixa-resolver/tests/git_integration.rs`)
4634 /// is accepted trivially — the gate is a no-op when the author
4635 /// didn't declare a value. `Some("")` is gated by the narrower
4636 /// [`ManifestError::EdicaoEmpty`] arm, mirroring the empty-arm
4637 /// shape every peer per-axis empty gate uses
4638 /// ([`ManifestError::NomeEmpty`], [`ManifestError::VersaoEmpty`],
4639 /// [`ManifestError::EtiquetaEmpty`], [`ManifestError::AutorEmpty`],
4640 /// [`ManifestError::RepositorioEmpty`],
4641 /// [`ManifestError::DescricaoEmpty`], [`ManifestError::LicencaEmpty`]).
4642 ///
4643 /// Universal-axis (every kind carries `:edicao`), so wired at
4644 /// the caixa-build gate alongside the peer universal gates
4645 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4646 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
4647 /// [`Self::validate_autores`] / [`Self::validate_repositorio`] /
4648 /// [`Self::validate_descricao`] / [`Self::validate_licenca`] /
4649 /// [`Self::validate_code_paths`] — before the kind-coherence
4650 /// gates ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4651 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4652 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4653 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-
4654 /// specific slot sets.
4655 ///
4656 /// Past the empty arm the gate enforces the canonical year-shape
4657 /// predicate: every documented tatara-lisp edition is a 4-digit
4658 /// ASCII decimal year (`"2026"` is the only edition currently
4659 /// minted; future-introduced siblings will follow the same
4660 /// shape, peer with Cargo's `[package] edition` grammar which
4661 /// every value Cargo has ever accepted matches — `"2015"`,
4662 /// `"2018"`, `"2021"`, `"2024"`). Any value that's not exactly
4663 /// 4 ASCII decimal bytes is rejected with the narrower
4664 /// [`ManifestError::EdicaoInvalid`] arm, mirroring the
4665 /// shape-predicate cascade [`Self::validate_repositorio`]
4666 /// establishes past its own empty arm
4667 /// ([`ManifestError::RepositorioEmpty`] →
4668 /// [`ManifestError::RepositorioInvalid`]). Closes the canonical
4669 /// paste-from-doc footguns the bare empty-arm gate left open:
4670 ///
4671 /// - leading / trailing whitespace from a paste-from-doc
4672 /// (`"2026 "`, `" 2026"`)
4673 /// - control characters / CRLF from a paste-from-multiline-doc
4674 /// (`"2026\n"`)
4675 /// - non-ASCII look-alikes from a fullwidth keyboard
4676 /// (`"2026"`) which would silently land as a non-ASCII
4677 /// string in the rendered caixa.lisp
4678 /// - free-form non-year values (`"x"`, `"latest"`,
4679 /// `"nightly"`) that have no operational meaning on the
4680 /// substrate's build-time edition selector
4681 /// - leading non-digit prefixes (`"v2026"`, `"e2026"`,
4682 /// `"r2026"`) — common version-tag idioms that don't apply
4683 /// to the year-shaped edition axis
4684 /// - decimal-shaped values (`"2026.1"`, `"2026.0"`) — every
4685 /// edition is a year, not a fractional version
4686 /// - wrong-length numeric values (`"26"`, `"202"`, `"20260"`,
4687 /// `"00026"`) that don't name a year
4688 ///
4689 /// `None` (the canonical "omit the slot to defer to the
4690 /// substrate's default edition" shape every existing
4691 /// [`caixa-resolver`] integration test fixture carries via
4692 /// `edicao: None` — see `caixa-resolver/tests/git_integration.rs`)
4693 /// is accepted trivially — the gate is a no-op when the author
4694 /// didn't declare a value. The empty-first cascade discipline
4695 /// mirrors every peer per-axis identity gate:
4696 /// [`ManifestError::EdicaoEmpty`] runs before
4697 /// [`ManifestError::EdicaoInvalid`], so the narrower empty
4698 /// diagnostic surfaces on `Some("")` rather than the broader
4699 /// shape-predicate diagnostic — peer with how
4700 /// [`ManifestError::NomeEmpty`] runs before
4701 /// [`ManifestError::NomeInvalid`],
4702 /// [`ManifestError::VersaoEmpty`] runs before
4703 /// [`ManifestError::VersaoInvalid`],
4704 /// [`ManifestError::RepositorioEmpty`] runs before
4705 /// [`ManifestError::RepositorioInvalid`].
4706 ///
4707 /// A future tightening on this axis can extend the shape
4708 /// predicate into a known-edition allowlist (rejecting
4709 /// year-shaped values that don't name a tatara-lisp edition
4710 /// the substrate actually understands — e.g., `"1999"` is
4711 /// year-shaped but no `1999` edition exists). That allowlist
4712 /// only becomes meaningful past the introduction of a sibling
4713 /// edition to `"2026"`; this gate establishes the structural
4714 /// floor by refusing every non-year-shaped value at validate
4715 /// time.
4716 pub fn validate_edicao(&self) -> Result<(), ManifestError> {
4717 let Some(s) = self.edicao() else {
4718 return Ok(());
4719 };
4720 if s.is_empty() {
4721 return Err(ManifestError::EdicaoEmpty);
4722 }
4723 if s.len() != 4 || !s.bytes().all(|b| b.is_ascii_digit()) {
4724 return Err(ManifestError::EdicaoInvalid {
4725 edicao: s.to_string(),
4726 reason: "must be a 4-digit ASCII decimal year (canonical \"2026\")".to_string(),
4727 });
4728 }
4729 Ok(())
4730 }
4731
4732 /// Compose the supervisor-related flat slots into a single
4733 /// [`SupervisorSpec`] for validation. Returns `None` when the
4734 /// caixa isn't a `:kind Supervisor`.
4735 ///
4736 /// The flat representation in [`Caixa`] keeps tatara-lisp authoring
4737 /// simple (one form, no nested `:supervisor (…)` block); this view
4738 /// is the "typed shape" the operator + supervisor reconciler
4739 /// consume.
4740 #[must_use]
4741 pub fn supervisor_view(&self) -> Option<SupervisorSpec> {
4742 if !self.kind().is_supervisor() {
4743 return None;
4744 }
4745 // Fold through the shared `supervisor::duration_codec::parse`
4746 // — the same parser the serde-routed `with = "duration_codec"`
4747 // on `SupervisorSpec::restart_window`, the `:politicas
4748 // :timeout` codec, and the `:politicas :circuit-breaker
4749 // :window` codec all consume. The prior inline f64-shaped
4750 // duplicate (`parse_window_inline`) admitted every magnitude
4751 // the integer-magnitude gate (1c55a2a) rejects on the three
4752 // serde-routed siblings — `"1.5s"`, `"1.0s"`, `"0.5m"`,
4753 // `"+30s"`, `"-30s"` — and silently dropped malformed input as
4754 // `None` (i.e. "no reset"), divergent from the shared codec's
4755 // integer-magnitude discipline by construction. The fold
4756 // closes the divergence: every value the typed
4757 // `SupervisorSpec` carries past `supervisor_view` is in the
4758 // shared codec's accepted set. The `.ok()` here preserves the
4759 // existing soft-swallow shape on this view-construction path;
4760 // the new [`Caixa::validate_restart_window`] (sibling of
4761 // [`Self::validate_nome`] / [`Self::validate_versao`]) names
4762 // the offending raw string at build time so authoring tools
4763 // (`feira lint`, the future layout-side wire-up) surface a
4764 // self-locating diagnostic instead of a silently dropped
4765 // window.
4766 let restart_window = self
4767 .restart_window()
4768 .and_then(|s| crate::supervisor::duration_codec::parse(s).ok());
4769 Some(SupervisorSpec {
4770 estrategia: self.estrategia().unwrap_or_default(),
4771 max_restarts: self.max_restarts().unwrap_or(5),
4772 restart_window,
4773 children: self.children().to_vec(),
4774 })
4775 }
4776
4777 /// A minimal starter manifest emitted by `feira init`.
4778 #[must_use]
4779 pub fn template(nome: &str) -> String {
4780 format!(
4781 "(defcaixa\n \
4782 :nome {nome:?}\n \
4783 :versao \"0.1.0\"\n \
4784 :kind Biblioteca\n \
4785 :edicao \"2026\"\n \
4786 :descricao \"FIXME — describe this caixa\"\n \
4787 :autores ()\n \
4788 :etiquetas ()\n \
4789 :deps ()\n \
4790 :deps-dev ()\n \
4791 :bibliotecas (\"lib/{nome}.lisp\"))\n"
4792 )
4793 }
4794
4795 /// Serialize to a canonical `caixa.lisp` source — suitable for writing
4796 /// back after mutation (e.g. `feira add`).
4797 ///
4798 /// Goes through serde JSON → canonical Sexp → per-field pretty print.
4799 /// The derive-macro `compile_from_sexp` path is the inverse, so any
4800 /// `Caixa` round-trips through `to_lisp` + `from_lisp`.
4801 #[must_use]
4802 pub fn to_lisp(&self) -> String {
4803 let json = serde_json::to_value(self).expect("Caixa serialize");
4804 let sexp = tatara_lisp::domain::json_to_sexp(&json);
4805 let tatara_lisp::Sexp::List(items) = sexp else {
4806 return format!("(defcaixa {sexp})\n");
4807 };
4808 let mut out = String::from("(defcaixa");
4809 let mut i = 0;
4810 while i + 1 < items.len() {
4811 out.push_str("\n ");
4812 out.push_str(&items[i].to_string());
4813 out.push(' ');
4814 out.push_str(&items[i + 1].to_string());
4815 i += 2;
4816 }
4817 out.push_str(")\n");
4818 out
4819 }
4820}
4821
4822/// Errors raised by top-level [`Caixa`] validators that don't fit
4823/// the per-axis [`DepError`] / [`crate::AplicacaoError`] /
4824/// [`crate::SupervisorError`] / [`crate::LayoutError`] families —
4825/// the Caixa's own identity axes (`:nome`, `:versao`) that flow
4826/// through every substrate-side artifact's `metadata.name` /
4827/// version derivation.
4828///
4829/// A future top-level sum (the M4 `CaixaError` the [`DepError`]
4830/// doc-comment anticipates) can hold one of each per-axis error
4831/// family without reshaping individual diagnostics; this enum is
4832/// the first such per-Caixa-identity family.
4833#[derive(Debug, Error, PartialEq, Eq)]
4834pub enum ManifestError {
4835 #[error(
4836 ":nome is empty (every caixa must name itself; the value flows \
4837 into every K8s artifact's `metadata.name` derivation and into \
4838 the default `lib/<nome>.lisp` / `exe/<nome>` layout paths)"
4839 )]
4840 NomeEmpty,
4841 #[error(
4842 ":nome {nome:?} is not a valid DNS-1123 label: {reason} (the K8s \
4843 apiserver enforces this rule on every `metadata.name` the \
4844 caixa's substrate-side renderers derive from `:nome` — the \
4845 `lareira-<nome>` Helm chart name, the programs.yaml entry \
4846 name, the `LABEL_APLICACAO` label value, the `<aplicacao>-<de>-to-<para>` \
4847 CiliumNetworkPolicy name, the `<aplicacao>-<para>` HTTPRoute \
4848 name; use a lowercase alphanumeric + hyphen identifier like \
4849 `\"checkout\"` or `\"cart-v2\"`)"
4850 )]
4851 NomeInvalid { nome: String, reason: String },
4852 #[error(
4853 ":nome {nome:?} overflows the joint-length budget on the canonical \
4854 `lareira-<nome>` chart-name shape: {reason} (every per-Servico / \
4855 per-Aplicacao renderer the substrate carries — `caixa-helm`'s \
4856 `Chart.yaml::name`, `caixa-flux`'s `cluster_bundle` HelmRelease \
4857 `chart:` slot, `caixa-tatara`'s `release_name` + \
4858 `oci://<registry>/lareira-<nome>` chart ref — derives the same \
4859 joint name through the canonical `lareira_chart_name` helper, and \
4860 Helm's `Chart.yaml::name` admission rule + the K8s apiserver's \
4861 DNS-1123 label cap on every chart-name-derived `metadata.name` \
4862 reject any joint name exceeding 63 bytes; the narrower \
4863 `:nome` shape (`NomeInvalid`) gates the bare-`:nome` budget, this \
4864 arm gates the chart-name budget downstream renderers inherit)"
4865 )]
4866 NomeChartNameBudgetExceeded { nome: String, reason: String },
4867 #[error(
4868 ":versao is empty (every caixa must pin its own version; the value flows \
4869 into the `lareira-<nome>` Helm chart's `Chart.yaml` version + appVersion, \
4870 the `feira publish` `v<versao>` git tag, the OCI image's `:v<versao>` / \
4871 `:latest` tags, the lacre closure's `concrete_versao`, and the \
4872 `:upgrade-from :from` peers — use a SemVer-2 literal like `\"0.1.0\"`)"
4873 )]
4874 VersaoEmpty,
4875 #[error(
4876 ":versao {versao:?} is not a valid SemVer-2 version: {reason} (the substrate \
4877 consumes this string as `semver::Version` — three-part `MAJOR.MINOR.PATCH` \
4878 with optional `-prerelease` and `+build` — across every artifact derived \
4879 from `:versao`: the `lareira-<nome>` Helm chart's `Chart.yaml` version + \
4880 appVersion (Helm SemVer-2-strict), the `feira publish` `v<versao>` git tag, \
4881 the OCI image's `:v<versao>` tag, the lacre closure's `concrete_versao`, \
4882 and the `:upgrade-from :from` peers that match against this exact shape; \
4883 use a literal like `\"0.1.0\"`, `\"0.2.0-rc.1\"`, or `\"1.0.0+build.42\"` — \
4884 not a git-tag-shape like `\"v0.1.0\"`, a docker-tag-shape like `\"latest\"`, \
4885 a requirement-shape like `\"^0.1\"`, or a four-part `\"0.1.0.0\"`)"
4886 )]
4887 VersaoInvalid { versao: String, reason: String },
4888 #[error(
4889 ":restart-window {restart_window:?} is not a valid duration: {reason} (the \
4890 substrate consumes this string through the shared \
4891 `supervisor::duration_codec` — the same parser routed via `with = \
4892 \"duration_codec\"` onto the typed `SupervisorSpec::restart_window`, \
4893 `:politicas :timeout`, and `:politicas :circuit-breaker :window` slots; \
4894 the canonical authoring form is `<integer><unit>` where the unit is one \
4895 of `ms` / `s` / `m` / `h` and the magnitude has no decimal point and no \
4896 leading `+` / `-` sign — e.g. `\"60s\"`, `\"5m\"`, `\"1h\"`, `\"500ms\"`. \
4897 Without this gate a malformed `:restart-window` silently produced a \
4898 supervisor with `restart_window: None` (\"never reset\"), turning OTP's \
4899 `MaxIntensity / Period` invariant into a never-reset supervisor far from \
4900 the source `caixa.lisp`; the gate moves the diagnostic to the manifest \
4901 layer with the offending value named verbatim. Omit the slot entirely to \
4902 express \"no reset\"; carry a positive integer duration to express the \
4903 sliding window)"
4904 )]
4905 RestartWindowMalformed {
4906 restart_window: String,
4907 reason: String,
4908 },
4909 #[error(
4910 "{slot} entry is an empty path string — every {slot} entry must name \
4911 a file relative to the caixa root; omit the entry to omit the file \
4912 (the layout checker's `root.join(\"\")` resolves to the caixa root \
4913 itself, so an empty entry silently aliases the project root as a \
4914 declared {slot} file, then fails downstream at parse / existence \
4915 time with a diagnostic that names the root rather than the offending \
4916 entry)"
4917 )]
4918 CodePathEmpty { slot: &'static str },
4919 #[error(
4920 "{slot} entry {} is an absolute path — entries must be relative to \
4921 the caixa root, since `Path::join` replaces the base with an absolute \
4922 right-hand side and `root.join(\"/abs/...\")` resolves to \"/abs/...\" \
4923 outside the caixa root sandbox; rewrite the entry as a relative path \
4924 under the caixa root (e.g. `\"lib/<name>.lisp\"`, `\"exe/<name>\"`, \
4925 `\"servicos/<name>.computeunit.yaml\"`)",
4926 path.display()
4927 )]
4928 CodePathAbsolute { slot: &'static str, path: PathBuf },
4929 #[error(
4930 "{slot} entry {} contains a `..` component — entries must not traverse \
4931 above the caixa root (the layout's `starts_with(<dir>)` fence on \
4932 `:exe` / `:servicos` is component-aware, not canonical-path-aware, \
4933 so a mid-path `..` silently traverses the sandbox; `:bibliotecas` \
4934 has no such fence, so a leading `..` escapes unconditionally if the \
4935 resolved target happens to exist)",
4936 path.display()
4937 )]
4938 CodePathParentEscape { slot: &'static str, path: PathBuf },
4939 #[error(
4940 "{slot} entry {} does not terminate in the `.lisp` extension — every \
4941 `:bibliotecas` entry is a tatara-lisp source file the `feira build` \
4942 loop reads through `tatara_lisp::read` at parse time, so any other \
4943 extension (`.rs`, `.txt`, `.lisp.bak`) or no-extension shape is \
4944 structurally a parser error far from the source caixa.lisp, with \
4945 no field naming the offending `:bibliotecas` entry. Pin a relative \
4946 path under the caixa root whose terminating extension is \
4947 lowercase-`.lisp` (e.g. `\"lib/<name>.lisp\"`, \
4948 `\"lib/handlers.lisp\"`) — the same file-type contract the peer \
4949 `:behavior :on-*` (c97815a) and `:upgrade-from :state-change :script` \
4950 (33cc830) axes already carry through the same lifted \
4951 `is_lisp_extension` predicate",
4952 path.display()
4953 )]
4954 CodePathNonLispExtension { slot: &'static str, path: PathBuf },
4955 #[error(
4956 "{slot} entry {} does not terminate in the `.computeunit.yaml` \
4957 compound suffix — every `:servicos` entry is a typed `ComputeUnit` \
4958 CR YAML file the peer caixa-helm / caixa-flux renderers consume \
4959 through `serde_yaml::from_str` at chart / FluxCD bundle render \
4960 time, so any other extension (`.yaml`, `.yml`, `.json`, the \
4961 off-by-one-segment `.computeunit-yaml`, the editor-backup \
4962 `.computeunit.yaml.bak`) or no-extension shape is structurally a \
4963 YAML-parser error / `ComputeUnit` schema-mismatch far from the \
4964 source caixa.lisp, with no field naming the offending `:servicos` \
4965 entry. Pin a relative path under the caixa root whose terminating \
4966 compound suffix is lowercase-`.computeunit.yaml` (e.g. \
4967 `\"servicos/<name>.computeunit.yaml\"`, \
4968 `\"servicos/hello-rio.computeunit.yaml\"`) — the same file-type \
4969 contract the sibling `:bibliotecas` axis (64772a9) already carries \
4970 on the tatara-lisp-source axis through the peer lifted \
4971 `is_lisp_extension` predicate, here on the compound-suffix axis \
4972 `Path::extension` can't express on its own through the lifted \
4973 `is_computeunit_yaml_extension` predicate",
4974 path.display()
4975 )]
4976 CodePathNonComputeUnitYamlExtension { slot: &'static str, path: PathBuf },
4977 #[error(
4978 "{slot} entry {} appears more than once (the code-path list is \
4979 a set, not a multiset; every peer Vec-shaped author-supplied \
4980 list past validate is set-not-multiset — `:membros :caixa`, \
4981 `:placement :clusters`, `:entrada :paths`, `:contratos`, \
4982 `:children :caixa`, `:deps` / `:deps-dev` `:nome`, \
4983 `:upgrade-from :from`, `:etiquetas`, `:autores` — and the three \
4984 code-path lists are the last Vec-shaped author-supplied slots on \
4985 the typed Caixa surface still admitting a duplicate entry. \
4986 `:bibliotecas` duplicates re-parse the same file at \
4987 `feira build` time and silently mask the author's intent to \
4988 declare a *second* biblioteca; `:exe` duplicates collide on the \
4989 flake `packages.<name>` derivation key at the future \
4990 `caixa-flake` materializer; `:servicos` duplicates surface as the \
4991 narrower [`caixa-helm`] / [`caixa-flux`] `UnsupportedServicoCount` \
4992 rejection far from the source `caixa.lisp`. Drop the duplicate \
4993 or rename it to the actual second file intended)",
4994 path.display()
4995 )]
4996 CodePathDuplicate { slot: &'static str, path: PathBuf },
4997 #[error(
4998 ":etiquetas entry is empty (every tag must carry a non-empty \
4999 registry-search identifier; the empty entry has no operational \
5000 meaning — it indexes nothing in the future caixa-registry search \
5001 axis and clutters the rendered Helm `Chart.yaml` `keywords:` array \
5002 with a no-op tag; omit the entry to express \"no tag on this \
5003 position\")"
5004 )]
5005 EtiquetaEmpty,
5006 #[error(
5007 ":etiquetas entry {etiqueta:?} appears more than once (the \
5008 registry-search tag set is a set, not a multiset; duplicate \
5009 entries are silently dedup'd by caixa-helm's `BTreeSet` collect \
5010 at chart render — a \"second wins / one silently disappears\" \
5011 shape divergent from every peer typed-graph set gate \
5012 (`:membros :caixa`, `:placement :clusters`, `:entrada :paths`, \
5013 `:contratos`, `:deps :nome`, `:upgrade-from :from`); drop the \
5014 duplicate or rename it to the actual tag intended)"
5015 )]
5016 EtiquetaDuplicate { etiqueta: String },
5017 #[error(
5018 ":etiquetas entry {etiqueta:?} is not a valid chart-keyword shape: \
5019 {reason} (the substrate consumes this string through the shared \
5020 `crate::render::is_chart_keyword_shape` predicate — the same \
5021 Cargo crates.io `[package] keywords` grammar entry shape: 1..=20 \
5022 bytes, starts with an ASCII letter, ASCII alphanumeric / `_` / `-` \
5023 continuation. The canonical authoring shapes are short kebab-case \
5024 identifiers like `\"mesh\"`, `\"wasm\"`, `\"tatara-lisp\"`, \
5025 `\"hello-world\"`, `\"caixa-servico\"`, `\"infrastructure\"`. \
5026 Without this gate a malformed `:etiquetas` entry (paste-from-doc \
5027 leading / trailing whitespace `\" mesh\"` / `\"mesh \"`; \
5028 paste-from-multiline-doc newline `\"mesh\\nhttp\"`; \
5029 paste-from-Windows-CRLF-doc CR; CSV-list-separator confusion \
5030 `\"mesh,http,grpc\"` — the author meant to author three separate \
5031 list entries; path-separator confusion `\"caixa/servico\"`; \
5032 namespace-suffix `\"http.1\"`; leading-digit `\"1foo\"`; \
5033 kebab-leak `\"-foo\"`; snake-leak `\"_foo\"`; non-ASCII \
5034 `\"café\"` — every legitimate search tag is strict ASCII; \
5035 paste-from-binary-blob NUL / BEL / ESC / DEL byte) silently \
5036 passed `from_lisp` + `validate_etiquetas` + \
5037 `StandardLayout::verify` and landed in the rendered \
5038 `lareira-<nome>` Helm chart's `Chart.yaml keywords:` array as a \
5039 malformed search tag — Artifact Hub's keyword index + the future \
5040 caixa-registry's keyword index would either silently drop the \
5041 tag or fail to index it far from the source caixa.lisp; the gate \
5042 moves the diagnostic to the manifest layer with the offending \
5043 value named verbatim)"
5044 )]
5045 EtiquetaInvalid { etiqueta: String, reason: String },
5046 #[error(
5047 ":autores entry is empty (every maintainer must carry a non-empty \
5048 identifier; the empty entry has no operational meaning — it \
5049 identifies no one in the substrate's authorship index and renders \
5050 as `maintainers: [{{name: \"\", email: null}}]` in the Helm chart's \
5051 `Chart.yaml`, a no-op maintainer the substrate cannot route to; \
5052 omit the entry to express \"no maintainer on this position\")"
5053 )]
5054 AutorEmpty,
5055 #[error(
5056 ":autores entry {autor:?} appears more than once (the maintainer \
5057 set is a set, not a multiset; unlike `:etiquetas`, caixa-helm's \
5058 `maintainers:` rendering does *no* dedup — duplicate entries \
5059 stack verbatim in `Chart.yaml` as two identical \
5060 `Maintainer {{ name, email: None }}` records, divergent from every \
5061 peer typed-graph set gate (`:etiquetas`, `:membros :caixa`, \
5062 `:placement :clusters`, `:entrada :paths`, `:contratos`, \
5063 `:deps :nome`, `:upgrade-from :from`); drop the duplicate or \
5064 rename it to the actual author intended)"
5065 )]
5066 AutorDuplicate { autor: String },
5067 #[error(
5068 ":autores entry {autor:?} is not a valid chart-maintainer-name shape: \
5069 {reason} (the substrate consumes this string through the shared \
5070 `crate::render::is_chart_maintainer_name_shape` predicate — the same \
5071 single-line-UTF-8 floor every realistic chart maintainer name carries: \
5072 1..=128 bytes, no leading or trailing whitespace, no ASCII control \
5073 characters anywhere, Unicode bytes accepted. The canonical authoring \
5074 shapes are short single-line identifiers like `\"pleme-io\"`, \
5075 `\"Pleme Contributors\"`, `\"alice <alice@example.com>\"`, \
5076 `\"François Dupont\"`. Without this gate a malformed `:autores` entry \
5077 (paste-from-aligned-doc leading whitespace `\" pleme-io\"` / trailing \
5078 whitespace `\"pleme-io \"`; paste-from-multiline-doc newline \
5079 `\"alice\\nbob\"` — the author pasted a multi-line block of author \
5080 records into one entry instead of splitting into one entry per author; \
5081 paste-from-Windows-CRLF-doc carriage return `\"alice\\rbob\"`; \
5082 tab-from-aligned-doc `\"Pleme\\tContributors\"`; paste-from-binary-blob \
5083 NUL / BEL / ESC / DEL byte) silently passed `from_lisp` + \
5084 `validate_autores` + `StandardLayout::verify` and landed in the \
5085 rendered `lareira-<nome>` Helm chart's `Chart.yaml maintainers:` array \
5086 as a YAML-illegal multi-line scalar or a silently-trimmed whitespace \
5087 round-trip — every chart-aware UI (`helm list`, `helm search`, \
5088 Artifact Hub maintainer index) would render the maintainer name in a \
5089 single-line column far from the source caixa.lisp; the gate moves the \
5090 diagnostic to the manifest layer with the offending value named \
5091 verbatim)"
5092 )]
5093 AutorInvalid { autor: String, reason: String },
5094 #[error(
5095 ":repositorio is the empty string (every published caixa names its \
5096 git source via a non-empty `:repositorio` locator — the value \
5097 flows verbatim into the rendered `lareira-<nome>` Helm chart's \
5098 `Chart.yaml` `home:` field via `caixa-helm` and into the FluxCD \
5099 `GitRepository.spec.url` via `caixa-flux`'s \
5100 `ClusterBundleOpts::for_caixa`; both consumers' \
5101 `Option::unwrap_or_else` fallbacks only fire when the slot is \
5102 `None`, so an empty `Some(\"\")` silently lands as `home: \"\"` / \
5103 `url: \"\"` in the rendered artifacts and breaks at `helm \
5104 template` / FluxCD source-controller reconcile time far from the \
5105 source caixa.lisp; omit the slot entirely to defer to the \
5106 renderer's `https://github.com/pleme-io/<nome>` / \
5107 `caixa.nome`-derived fallback, or carry a canonical authoring \
5108 shape like `\"github:org/repo\"`, `\"https://host/path\"`, \
5109 `\"ssh://[user@]host/path\"`, `\"git@host:path\"`, or \
5110 `\"file:///path\"`)"
5111 )]
5112 RepositorioEmpty,
5113 #[error(
5114 ":repositorio {repositorio:?} is not a valid git repo URL: {reason} \
5115 (the substrate consumes this string through the shared \
5116 `crate::render::is_git_repo_url` predicate — the same parser the \
5117 peer `:deps :fonte (:tipo git :repo …)` axis routes its `:repo` \
5118 value through via `DepSource::validate`; the canonical authoring \
5119 shapes are `\"github:org/repo\"` shorthand, `\"https://host/path\"` \
5120 / `\"ssh://[user@]host/path\"` / `\"git://host/path\"` / \
5121 `\"file:///path\"` URL schemes, or the `\"git@host:path\"` \
5122 scp-style SSH form. Without this gate a malformed `:repositorio` \
5123 (whitespace from a paste-from-doc; control characters / CRLF \
5124 from a paste-from-multiline-doc; a leading `-` from a \
5125 CLI-argument-injection footgun; a missing `:` separator from a \
5126 bare `org/repo` shape git treats as a relative filesystem path) \
5127 silently landed in the rendered `Chart.yaml home:` and the \
5128 FluxCD `GitRepository.spec.url` and broke at `git clone` / \
5129 FluxCD reconcile time far from the source caixa.lisp; the gate \
5130 moves the diagnostic to the manifest layer with the offending \
5131 value named verbatim)"
5132 )]
5133 RepositorioInvalid { repositorio: String, reason: String },
5134 #[error(
5135 ":descricao is the empty string (every published caixa names \
5136 its purpose via a non-empty `:descricao` summary — the value \
5137 flows verbatim into the rendered `lareira-<nome>` Helm \
5138 chart's `Chart.yaml` `description:` field via `caixa-helm`'s \
5139 `build_chart_yaml` and into the chart `README.md` header via \
5140 `build_readme`; both consumers' `Option::unwrap_or_else` \
5141 `caixa.nome`-derived fallbacks only fire when the slot is \
5142 `None`, so an empty `Some(\"\")` silently lands as \
5143 `description: \"\"` / a blank `README.md` header in the \
5144 rendered artifacts and breaks at `helm lint` time \
5145 (`WARNING [chart.metadata.description]: description is \
5146 required` on `apiVersion: v2` charts) far from the source \
5147 caixa.lisp; omit the slot entirely to defer to the \
5148 renderer's `\"Generated chart for caixa Servico <nome>\"` / \
5149 `\"caixa Servico <nome>\"` fallbacks, or carry a non-empty \
5150 summary like `\"Canonical Rust→wasm32-wasip2 caixa \
5151 Servico.\"`)"
5152 )]
5153 DescricaoEmpty,
5154 #[error(
5155 ":descricao {descricao:?} is not a valid chart-description shape: \
5156 {reason} (the substrate consumes this string through the shared \
5157 `crate::render::is_chart_description_shape` predicate — the same \
5158 single-line-UTF-8 floor every realistic chart description carries: \
5159 1..=512 bytes, no leading or trailing whitespace, no ASCII control \
5160 characters anywhere, Unicode prose bytes accepted. The canonical \
5161 authoring shapes are short single-line summaries like `\"Canonical \
5162 Rust→wasm32-wasip2 caixa Servico.\"`, `\"Checkout flow.\"`, \
5163 `\"AWS provider caixa for tatara-lisp\"`. Without this gate a \
5164 malformed `:descricao` (paste-from-aligned-doc leading whitespace \
5165 `\" Checkout flow.\"` / trailing whitespace `\"Checkout flow. \"`; \
5166 paste-from-multiline-doc newline `\"Checkout\\nflow.\"`; \
5167 paste-from-Windows-CRLF-doc carriage return `\"Checkout\\rflow.\"`; \
5168 tab-from-aligned-doc `\"Checkout\\tflow.\"`; paste-from-binary-blob \
5169 NUL / BEL / ESC / DEL byte) silently passed `from_lisp` + \
5170 `validate_descricao` + `StandardLayout::verify` and landed in the \
5171 rendered `lareira-<nome>` Helm chart's `Chart.yaml description:` \
5172 field + `README.md` header paragraph as a YAML-illegal multi-line \
5173 scalar or a silently-trimmed whitespace round-trip — every \
5174 chart-aware UI (`helm list`, `helm search`, Artifact Hub) would \
5175 render the description in a single-line column far from the source \
5176 caixa.lisp; the gate moves the diagnostic to the manifest layer \
5177 with the offending value named verbatim)"
5178 )]
5179 DescricaoInvalid { descricao: String, reason: String },
5180 #[error(
5181 ":licenca is the empty string (every published caixa names \
5182 its license via a non-empty `:licenca` SPDX expression — the \
5183 value flows verbatim into the rendered `lareira-<nome>` Helm \
5184 chart's `README.md` `## License` section via `caixa-helm`'s \
5185 `build_readme` at `caixa-helm/src/lib.rs:361`; the consumer's \
5186 `Option::unwrap_or_else(|| \"MIT\".into())` `MIT` fallback \
5187 only fires when the slot is `None`, so an empty `Some(\"\")` \
5188 silently lands as a bare trailing period in the rendered \
5189 chart `README.md` `License` section far from the source \
5190 caixa.lisp; omit the slot entirely to defer to the \
5191 renderer's `MIT` fallback, or carry a canonical SPDX \
5192 expression like `\"MIT\"`, `\"Apache-2.0\"`, \
5193 `\"Apache-2.0 OR MIT\"`)"
5194 )]
5195 LicencaEmpty,
5196 #[error(
5197 ":licenca {licenca:?} is not a valid SPDX expression shape: {reason} \
5198 (the substrate consumes this string through the shared \
5199 `crate::render::is_spdx_expression_shape` predicate — the same \
5200 alphabet-floor parser every peer per-axis value-shape gate routes \
5201 its value through; the canonical authoring shapes are single \
5202 license identifiers like `\"MIT\"`, `\"Apache-2.0\"`, `\"BSD-3-Clause\"`, \
5203 compound expressions like `\"Apache-2.0 OR MIT\"`, \
5204 `\"MIT AND BSD-3-Clause\"`, `\"(MIT OR Apache-2.0) AND ISC\"`, \
5205 license-with-exception forms like `\"Apache-2.0 WITH LLVM-exception\"`, \
5206 `+`-suffix variants like `\"GPL-2.0+\"`, and user-defined references \
5207 like `\"LicenseRef-MyLicense\"` / \
5208 `\"DocumentRef-doc:LicenseRef-MyLicense\"`. Without this gate a \
5209 malformed `:licenca` (paste-from-doc whitespace `\"MIT \"` / \
5210 `\" MIT\"`; paste-from-multiline-doc CRLF `\"MIT\\n\"`; \
5211 tab-from-aligned-doc `\"MIT\\tOR Apache-2.0\"`; non-ASCII byte from \
5212 a smart-quote paste; underscore-instead-of-hyphen typo \
5213 `\"Apache_2.0\"`; comma-instead-of-`OR`-keyword colloquial idiom \
5214 `\"MIT, Apache-2.0\"`; slash-dual-license colloquial idiom \
5215 `\"MIT/Apache-2.0\"`; semicolon-list-separator confusion \
5216 `\"MIT; Apache-2.0\"`) silently landed in the rendered chart \
5217 `README.md` `## License` section + a future SPDX-aware \
5218 `Chart.yaml license:` emitter would refuse the value at \
5219 `helm lint` time far from the source caixa.lisp; the gate moves \
5220 the diagnostic to the manifest layer with the offending value \
5221 named verbatim)"
5222 )]
5223 LicencaInvalid { licenca: String, reason: String },
5224 #[error(
5225 ":edicao is the empty string (every published caixa names \
5226 its language edition via a non-empty `:edicao` value — the \
5227 edition determines the tatara-lisp macro surface + \
5228 compatibility flags the substrate applies when building \
5229 the caixa; the canonical `Caixa::template` scaffold every \
5230 `feira init` emits carries `:edicao \"2026\"` verbatim and \
5231 every renderer-side fixture (`caixa-helm`, `caixa-flux`, \
5232 `caixa-mesh`) carries `edicao: Some(\"2026\".into())` by \
5233 construction, so an empty `Some(\"\")` silently lands as a \
5234 bare `(:edicao \"\")` line in the rendered `caixa.lisp` and \
5235 a future renderer-side consumer that folds it through \
5236 `Option::unwrap_or_else` will skip the fallback and pass the \
5237 empty edition through to the substrate's build-time edition \
5238 selector far from the source caixa.lisp; omit the slot \
5239 entirely to defer to the substrate's default edition, or \
5240 carry a canonical edition like `\"2026\"`)"
5241 )]
5242 EdicaoEmpty,
5243 #[error(
5244 ":edicao {edicao:?} is not a valid edition: {reason} (every \
5245 documented tatara-lisp edition is a 4-digit ASCII decimal \
5246 year — `\"2026\"` is the only edition currently minted; \
5247 future-introduced siblings will follow the same shape, peer \
5248 with Cargo's `[package] edition` grammar which every value \
5249 Cargo has ever accepted matches: `\"2015\"`, `\"2018\"`, \
5250 `\"2021\"`, `\"2024\"`. Without this gate the canonical \
5251 paste-from-doc footguns silently passed: a trailing space \
5252 (`\"2026 \"`) from a paste-from-doc, a CRLF (`\"2026\\n\"`) \
5253 from a paste-from-multiline-doc, a fullwidth-keyboard \
5254 look-alike (`\"2026\"`), a free-form non-year value \
5255 (`\"x\"`, `\"latest\"`, `\"nightly\"`), a leading non-digit \
5256 version-tag prefix (`\"v2026\"`, `\"e2026\"`), a \
5257 decimal-shaped pseudo-version (`\"2026.1\"`), or a \
5258 wrong-length numeric value (`\"26\"`, `\"202\"`, \
5259 `\"20260\"`) all landed as `(:edicao \"<garbage>\")` in the \
5260 rendered caixa.lisp and broke at the substrate's \
5261 build-time edition selector far from the source caixa.lisp; \
5262 omit the slot entirely to defer to the substrate's default \
5263 edition, or carry a canonical 4-digit ASCII decimal year \
5264 like `\"2026\"`)"
5265 )]
5266 EdicaoInvalid { edicao: String, reason: String },
5267}
5268
5269#[cfg(test)]
5270mod tests {
5271 use super::*;
5272
5273 #[test]
5274 fn template_round_trips() {
5275 let src = Caixa::template("demo");
5276 let c = Caixa::from_lisp(&src).expect("template must parse");
5277 assert_eq!(c.nome, "demo");
5278 assert_eq!(c.versao, "0.1.0");
5279 assert_eq!(c.kind, CaixaKind::Biblioteca);
5280 assert_eq!(c.bibliotecas, vec!["lib/demo.lisp".to_string()]);
5281 assert!(c.deps.is_empty());
5282 assert!(c.deps_dev.is_empty());
5283 }
5284
5285 #[test]
5286 fn register_populates_registry() {
5287 Caixa::register();
5288 let kws = tatara_lisp::domain::registered_keywords();
5289 assert!(kws.contains(&"defcaixa"));
5290 }
5291
5292 #[test]
5293 fn to_lisp_round_trips() {
5294 let src = Caixa::template("demo");
5295 let c1 = Caixa::from_lisp(&src).unwrap();
5296 let emitted = c1.to_lisp();
5297 let c2 = Caixa::from_lisp(&emitted).expect("emitted lisp parses back");
5298 assert_eq!(c1, c2);
5299 }
5300
5301 // ── M2 typed-substrate slot tests (limits, behavior, upgrade-from, supervisor) ──
5302
5303 #[test]
5304 fn limits_round_trip_via_json() {
5305 use crate::LimitsSpec;
5306 use std::time::Duration;
5307 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5308 c.limits = Some(LimitsSpec {
5309 memory: Some(64 * 1024 * 1024),
5310 fuel: Some(1_000_000),
5311 wall_clock: Some(Duration::from_secs(30)),
5312 cpu: Some(500),
5313 });
5314 let json = serde_json::to_string(&c).unwrap();
5315 assert!(json.contains("\"limits\""));
5316 assert!(json.contains("\"64MiB\""));
5317 assert!(json.contains("\"30s\""));
5318 assert!(json.contains("\"500m\""));
5319 let back: Caixa = serde_json::from_str(&json).unwrap();
5320 assert_eq!(c.limits, back.limits);
5321 }
5322
5323 #[test]
5324 fn behavior_round_trip_via_json() {
5325 use crate::BehaviorSpec;
5326 use std::path::PathBuf;
5327 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5328 c.behavior = Some(BehaviorSpec {
5329 on_init: Some(PathBuf::from("lib/init.lisp")),
5330 on_call: Some(PathBuf::from("lib/handlers.lisp")),
5331 ..Default::default()
5332 });
5333 let json = serde_json::to_string(&c).unwrap();
5334 let back: Caixa = serde_json::from_str(&json).unwrap();
5335 assert_eq!(c.behavior, back.behavior);
5336 }
5337
5338 #[test]
5339 fn upgrade_from_round_trip_via_json() {
5340 use crate::{UpgradeFromEntry, UpgradeInstruction};
5341 use std::path::PathBuf;
5342 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5343 c.upgrade_from = vec![UpgradeFromEntry {
5344 from: "0.1.0".into(),
5345 instructions: vec![
5346 UpgradeInstruction::LoadModule {
5347 module: "demo".into(),
5348 },
5349 UpgradeInstruction::StateChange {
5350 script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
5351 },
5352 UpgradeInstruction::SoftPurge {
5353 module: "demo-old".into(),
5354 },
5355 ],
5356 }];
5357 let json = serde_json::to_string(&c).unwrap();
5358 let back: Caixa = serde_json::from_str(&json).unwrap();
5359 assert_eq!(c.upgrade_from, back.upgrade_from);
5360 }
5361
5362 #[test]
5363 fn supervisor_view_returns_typed_shape() {
5364 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
5365 let mut c = Caixa::from_lisp(&Caixa::template("root")).unwrap();
5366 c.kind = CaixaKind::Supervisor;
5367 c.bibliotecas.clear();
5368 c.estrategia = Some(RestartStrategy::OneForOne);
5369 c.max_restarts = Some(5);
5370 c.restart_window = Some("60s".into());
5371 c.children = vec![ChildSpec {
5372 caixa: "worker".into(),
5373 versao: "^0.1".into(),
5374 restart: RestartPolicy::Permanent,
5375 }];
5376 let view = c.supervisor_view().expect("Supervisor kind has a view");
5377 assert_eq!(view.estrategia, RestartStrategy::OneForOne);
5378 assert_eq!(view.max_restarts, 5);
5379 assert_eq!(
5380 view.restart_window,
5381 Some(std::time::Duration::from_secs(60))
5382 );
5383 assert_eq!(view.children.len(), 1);
5384 view.validate().unwrap();
5385 }
5386
5387 #[test]
5388 fn supervisor_view_none_for_non_supervisor_kinds() {
5389 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5390 assert!(c.supervisor_view().is_none());
5391 }
5392
5393 #[test]
5394 fn declared_mesh_slots_empty_for_bare_caixa() {
5395 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5396 assert!(c.declared_mesh_slots().is_empty());
5397 }
5398
5399 #[test]
5400 fn declared_mesh_slots_reports_only_set_slots_in_canonical_order() {
5401 use crate::{Entrada, Membro};
5402 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5403 // Set a non-adjacent pair (:membros + :entrada) to pin that the
5404 // canonical declaration order is preserved regardless of which
5405 // subset is populated.
5406 c.membros = vec![Membro {
5407 caixa: "a".into(),
5408 versao: "^0.1".into(),
5409 }];
5410 c.entrada = Some(Entrada {
5411 host: "x.example.com".into(),
5412 para: "a".into(),
5413 paths: vec![],
5414 port: 8080,
5415 });
5416 assert_eq!(
5417 c.declared_mesh_slots(),
5418 vec![
5419 crate::render::M3_AUTHOR_KEY_MEMBROS,
5420 crate::render::M3_AUTHOR_KEY_ENTRADA,
5421 ]
5422 );
5423 }
5424
5425 #[test]
5426 fn m3_top_level_author_key_consts_pin_canonical_kebab_case_labels() {
5427 // Scalar-value pin: the five author-facing kebab-case labels the
5428 // `(defcaixa … :<slot> (…))` surface admits on the M3 top-level
5429 // mesh slot axis, one arm per typed slot. Mirrors the peer
5430 // scalar-value pin the sibling
5431 // [`crate::M2_AUTHOR_KEY_LIMITS`] /
5432 // [`crate::M2_AUTHOR_KEY_BEHAVIOR`] /
5433 // [`crate::M2_AUTHOR_KEY_UPGRADE_FROM`] M2 top-level slot consts
5434 // carry (f49c8b0), so both altitudes of the typed-slot algebra
5435 // (per-Servico M2 + per-Aplicacao M3) share the same
5436 // "one canonical byte-string per arm" discipline. A future
5437 // rebrand (`:membros` → `:members`, `:contratos` → `:contracts`,
5438 // `:politicas` → `:policies`, `:placement` → `:distribution`,
5439 // `:entrada` → `:ingress`) lands as an edit to exactly one const,
5440 // and every consumer that reaches for the label picks it up at
5441 // build time rather than at runtime as a downstream mismatch.
5442 assert_eq!(crate::render::M3_AUTHOR_KEY_MEMBROS, ":membros");
5443 assert_eq!(crate::render::M3_AUTHOR_KEY_CONTRATOS, ":contratos");
5444 assert_eq!(crate::render::M3_AUTHOR_KEY_POLITICAS, ":politicas");
5445 assert_eq!(crate::render::M3_AUTHOR_KEY_PLACEMENT, ":placement");
5446 assert_eq!(crate::render::M3_AUTHOR_KEY_ENTRADA, ":entrada");
5447 }
5448
5449 #[test]
5450 fn declared_mesh_slots_route_through_lifted_m3_author_key_consts() {
5451 // Production-through-const pin: the five per-arm labels the
5452 // [`Caixa::declared_mesh_slots`] tagger pushes onto its return
5453 // `Vec` route through the lifted
5454 // [`crate::M3_AUTHOR_KEY_MEMBROS`] /
5455 // [`crate::M3_AUTHOR_KEY_CONTRATOS`] /
5456 // [`crate::M3_AUTHOR_KEY_POLITICAS`] /
5457 // [`crate::M3_AUTHOR_KEY_PLACEMENT`] /
5458 // [`crate::M3_AUTHOR_KEY_ENTRADA`] consts, in canonical
5459 // declaration order. A future re-order or drift at the tagger
5460 // (a rename that reaches the tagger but not the const, or vice
5461 // versa) surfaces here at build time rather than at runtime as
5462 // a [`crate::LayoutError::MeshSlotsOnNonAplicacao`]
5463 // `slots: <stale-kebab-case>` diagnostic far from the rename's
5464 // commit. Mirror of the peer
5465 // [`declared_servico_slots_route_through_lifted_m2_author_key_consts`]
5466 // pin (f49c8b0) on the sibling per-Servico M2 top-level slot
5467 // axis.
5468 use crate::{Entrada, Membro, MeshPolicy, Placement, PlacementStrategy, WitContract};
5469 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5470 c.membros = vec![Membro {
5471 caixa: "a".into(),
5472 versao: "^0.1".into(),
5473 }];
5474 c.contratos = vec![WitContract {
5475 de: "a".into(),
5476 para: "a".into(),
5477 wit: "wasi:http/proxy".into(),
5478 endpoint: Some("/x".into()),
5479 subject: None,
5480 slot: None,
5481 }];
5482 c.politicas = Some(MeshPolicy::default());
5483 c.placement = Some(Placement {
5484 estrategia: PlacementStrategy::Replicated,
5485 clusters: vec!["rio".into()],
5486 affinity: None,
5487 shard_key: None,
5488 });
5489 c.entrada = Some(Entrada {
5490 host: "x.example.com".into(),
5491 para: "a".into(),
5492 paths: vec![],
5493 port: 8080,
5494 });
5495 assert_eq!(
5496 c.declared_mesh_slots(),
5497 vec![
5498 crate::render::M3_AUTHOR_KEY_MEMBROS,
5499 crate::render::M3_AUTHOR_KEY_CONTRATOS,
5500 crate::render::M3_AUTHOR_KEY_POLITICAS,
5501 crate::render::M3_AUTHOR_KEY_PLACEMENT,
5502 crate::render::M3_AUTHOR_KEY_ENTRADA,
5503 ]
5504 );
5505 }
5506
5507 #[test]
5508 fn declared_supervisor_slots_empty_for_bare_caixa() {
5509 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5510 assert!(c.declared_supervisor_slots().is_empty());
5511 }
5512
5513 #[test]
5514 fn declared_supervisor_slots_reports_only_set_slots_in_canonical_order() {
5515 use crate::RestartStrategy;
5516 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5517 // Set a non-adjacent pair (:estrategia + :restart-window) to pin
5518 // that the canonical declaration order is preserved regardless
5519 // of which subset is populated.
5520 c.estrategia = Some(RestartStrategy::OneForOne);
5521 c.restart_window = Some("60s".into());
5522 assert_eq!(
5523 c.declared_supervisor_slots(),
5524 vec![
5525 crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA,
5526 crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW,
5527 ]
5528 );
5529 }
5530
5531 #[test]
5532 fn supervisor_top_level_author_key_consts_pin_canonical_kebab_case_labels() {
5533 // Scalar-value pin: the four author-facing kebab-case labels the
5534 // `(defcaixa … :<slot> (…))` surface admits on the Supervisor
5535 // supervision-tree slot axis, one arm per typed slot. Mirrors the
5536 // peer scalar-value pins the sibling
5537 // [`crate::render::M2_AUTHOR_KEY_LIMITS`] /
5538 // [`crate::render::M2_AUTHOR_KEY_BEHAVIOR`] /
5539 // [`crate::render::M2_AUTHOR_KEY_UPGRADE_FROM`] top-level M2 slot
5540 // consts and [`crate::render::M3_AUTHOR_KEY_MEMBROS`] etc.
5541 // top-level M3 slot consts carry, so all three kind-scoped
5542 // typed-slot-family author-facing-label axes route through one
5543 // canonical per-arm declaration. A future rebrand
5544 // (`:estrategia` → `:strategy` for English uniformity,
5545 // `:max-restarts` → `:max-intensity` matching Erlang/OTP's
5546 // `MaxIntensity` name, `:restart-window` → `:period` matching
5547 // OTP's `Period` name, `:children` → `:workers` matching Elixir
5548 // idiom) lands as an edit to exactly one const, and every
5549 // consumer that reaches for the label picks it up at build time
5550 // rather than at runtime as a downstream mismatch.
5551 assert_eq!(
5552 crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA,
5553 ":estrategia"
5554 );
5555 assert_eq!(
5556 crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS,
5557 ":max-restarts"
5558 );
5559 assert_eq!(
5560 crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW,
5561 ":restart-window"
5562 );
5563 assert_eq!(crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN, ":children");
5564 }
5565
5566 #[test]
5567 fn declared_supervisor_slots_route_through_lifted_supervisor_author_key_consts() {
5568 // Production-through-const pin: the four per-arm labels the
5569 // [`Caixa::declared_supervisor_slots`] tagger pushes onto its
5570 // return `Vec` route through the lifted
5571 // [`crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA`] /
5572 // [`crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS`] /
5573 // [`crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW`] /
5574 // [`crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN`] consts, in
5575 // canonical declaration order. A future re-order or drift at the
5576 // tagger (a rename that reaches the tagger but not the const, or
5577 // vice versa) surfaces here at build time rather than at runtime
5578 // as a [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
5579 // `slots: <stale-kebab-case>` diagnostic far from the rename's
5580 // commit. Mirror of the peer
5581 // [`declared_servico_slots_route_through_lifted_m2_author_key_consts`]
5582 // (f49c8b0) and
5583 // [`declared_mesh_slots_route_through_lifted_m3_author_key_consts`]
5584 // (882f498) pins on the sibling M2 / M3 top-level slot axes.
5585 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
5586 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5587 c.estrategia = Some(RestartStrategy::OneForOne);
5588 c.max_restarts = Some(5);
5589 c.restart_window = Some("60s".into());
5590 c.children = vec![ChildSpec {
5591 caixa: "worker".into(),
5592 versao: "^0.1".into(),
5593 restart: RestartPolicy::Permanent,
5594 }];
5595 assert_eq!(
5596 c.declared_supervisor_slots(),
5597 vec![
5598 crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA,
5599 crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS,
5600 crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW,
5601 crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN,
5602 ]
5603 );
5604 }
5605
5606 #[test]
5607 fn declared_servico_slots_empty_for_bare_caixa() {
5608 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5609 assert!(c.declared_servico_slots().is_empty());
5610 }
5611
5612 #[test]
5613 fn declared_servico_slots_reports_only_set_slots_in_canonical_order() {
5614 use crate::{UpgradeFromEntry, UpgradeInstruction};
5615 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5616 // Set a non-adjacent pair (:limits + :upgrade-from) to pin that
5617 // the canonical declaration order is preserved regardless of
5618 // which subset is populated.
5619 c.limits = Some(crate::LimitsSpec {
5620 fuel: Some(1_000_000),
5621 ..Default::default()
5622 });
5623 c.upgrade_from = vec![UpgradeFromEntry {
5624 from: "0.1.0".into(),
5625 instructions: vec![UpgradeInstruction::Restart],
5626 }];
5627 assert_eq!(
5628 c.declared_servico_slots(),
5629 vec![
5630 crate::render::M2_AUTHOR_KEY_LIMITS,
5631 crate::render::M2_AUTHOR_KEY_UPGRADE_FROM,
5632 ]
5633 );
5634 }
5635
5636 #[test]
5637 fn m2_top_level_author_key_consts_pin_canonical_kebab_case_labels() {
5638 // Scalar-value pin: the three author-facing kebab-case labels
5639 // the `(defcaixa … :<slot> (…))` surface admits on the M2
5640 // top-level slot axis, one arm per typed slot. Mirrors the peer
5641 // scalar-value pin the sibling renderer-side
5642 // [`crate::M2_KEY_LIMITS`] / [`crate::M2_KEY_BEHAVIOR`] /
5643 // [`crate::M2_KEY_UPGRADE_FROM`] camelCase overlay-container
5644 // consts carry, so both halves of the M2 top-level slot dual
5645 // axis (author-facing kebab-case label + renderer-side
5646 // camelCase overlay-container wire key) route through one
5647 // canonical per-arm declaration. A future rebrand
5648 // (`:limits` → `:sandbox` matching Lunatic per-process
5649 // terminology INSPIRATIONS §III.1, `:behavior` → `:gen-server`
5650 // matching Erlang's verbatim name, `:upgrade-from` → `:appup`
5651 // matching Erlang's verbatim appup name) lands as an edit to
5652 // exactly one const, and every consumer that reaches for the
5653 // label picks it up at build time rather than at runtime as a
5654 // downstream mismatch.
5655 assert_eq!(crate::render::M2_AUTHOR_KEY_LIMITS, ":limits");
5656 assert_eq!(crate::render::M2_AUTHOR_KEY_BEHAVIOR, ":behavior");
5657 assert_eq!(crate::render::M2_AUTHOR_KEY_UPGRADE_FROM, ":upgrade-from");
5658 }
5659
5660 #[test]
5661 fn declared_servico_slots_route_through_lifted_m2_author_key_consts() {
5662 // Production-through-const pin: the three per-arm labels the
5663 // [`Caixa::declared_servico_slots`] tagger pushes onto its
5664 // return `Vec` route through the lifted
5665 // [`crate::M2_AUTHOR_KEY_LIMITS`] /
5666 // [`crate::M2_AUTHOR_KEY_BEHAVIOR`] /
5667 // [`crate::M2_AUTHOR_KEY_UPGRADE_FROM`] consts, in canonical
5668 // declaration order. A future re-order or drift at the tagger
5669 // (a rename that reaches the tagger but not the const, or vice
5670 // versa) surfaces here at build time rather than at runtime as
5671 // a [`crate::LayoutError::ServicoSlotsOnNonServico`]
5672 // `slots: <stale-kebab-case>` diagnostic far from the rename's
5673 // commit. Mirror of the peer
5674 // [`crate::behavior::BehaviorSpec::declared_slots`] production
5675 // tagger pin (889dc18) on the sibling per-callback axis.
5676 use crate::{BehaviorSpec, UpgradeFromEntry, UpgradeInstruction};
5677 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5678 c.limits = Some(crate::LimitsSpec {
5679 fuel: Some(1_000_000),
5680 ..Default::default()
5681 });
5682 c.behavior = Some(BehaviorSpec {
5683 on_init: Some(PathBuf::from("lib/init.lisp")),
5684 ..Default::default()
5685 });
5686 c.upgrade_from = vec![UpgradeFromEntry {
5687 from: "0.1.0".into(),
5688 instructions: vec![UpgradeInstruction::Restart],
5689 }];
5690 assert_eq!(
5691 c.declared_servico_slots(),
5692 vec![
5693 crate::render::M2_AUTHOR_KEY_LIMITS,
5694 crate::render::M2_AUTHOR_KEY_BEHAVIOR,
5695 crate::render::M2_AUTHOR_KEY_UPGRADE_FROM,
5696 ]
5697 );
5698 }
5699
5700 #[test]
5701 fn existing_manifests_unaffected_by_new_optional_slots() {
5702 // Regression test: a caixa.lisp authored before M2 typed slots
5703 // should still parse + serialize cleanly. The bare `defcaixa`
5704 // emitted by `Caixa::template` has none of the new fields.
5705 let src = Caixa::template("legacy");
5706 let c = Caixa::from_lisp(&src).unwrap();
5707 assert!(c.limits.is_none());
5708 assert!(c.behavior.is_none());
5709 assert!(c.upgrade_from.is_empty());
5710 assert!(c.estrategia.is_none());
5711 assert!(c.children.is_empty());
5712
5713 // And to_lisp emits a manifest with the new slots in the
5714 // empty/default state — round-trippable.
5715 let emitted = c.to_lisp();
5716 let back = Caixa::from_lisp(&emitted).unwrap();
5717 assert_eq!(c, back);
5718 }
5719
5720 #[test]
5721 fn validate_deps_accepts_canonical_caixa() {
5722 // Positive control: the bare template — zero deps, zero
5723 // deps_dev — passes the gate trivially. A future axis added to
5724 // `Dep::validate` mustn't regress an empty-deps caixa to a
5725 // build error.
5726 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5727 c.validate_deps().unwrap();
5728 }
5729
5730 #[test]
5731 fn validate_deps_rejects_invalid_versao_in_deps() {
5732 // Fail-before-pass-after pin: a malformed `:deps :versao`
5733 // surfaces at validate_deps() time, not at lacre-resolve time.
5734 // Mirrors `rejects_invalid_membro_versao_requirement` and
5735 // `validate_rejects_invalid_child_versao_requirement` on the
5736 // other two `:versao` axes.
5737 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5738 c.deps = vec![Dep::simple("caixa-teia", "^bad-version")];
5739 let err = c.validate_deps().unwrap_err();
5740 assert!(
5741 matches!(
5742 err,
5743 crate::dep::DepError::VersaoInvalid { ref nome, ref versao, .. }
5744 if nome == "caixa-teia" && versao == "^bad-version"
5745 ),
5746 "got {err:?}"
5747 );
5748 }
5749
5750 #[test]
5751 fn validate_deps_rejects_invalid_versao_in_deps_dev() {
5752 // Parity pin: `:deps-dev` must run through the same per-entry
5753 // validator as `:deps` — a typo in either axis surfaces the
5754 // same diagnostic. Without this leg, `:deps-dev` would be a
5755 // second-class citizen of the typed surface and an author
5756 // could land a build that passes validate_deps but fails at
5757 // `feira lock`-time when the dev-dep is resolved for a test
5758 // build.
5759 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5760 c.deps_dev = vec![Dep::simple("tatara-check", "^^0.1")];
5761 let err = c.validate_deps().unwrap_err();
5762 assert!(
5763 matches!(
5764 err,
5765 crate::dep::DepError::VersaoInvalid { ref nome, ref versao, .. }
5766 if nome == "tatara-check" && versao == "^^0.1"
5767 ),
5768 "got {err:?}"
5769 );
5770 }
5771
5772 #[test]
5773 fn validate_deps_runs_deps_before_deps_dev() {
5774 // Order pin: when both lists carry typos, the `:deps`
5775 // diagnostic surfaces first. The author's mental model is
5776 // "runtime deps are load-bearing; dev deps are scaffolding";
5777 // surfacing the runtime axis first matches that hierarchy.
5778 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5779 c.deps = vec![Dep::simple("runtime-dep", "^bad-runtime")];
5780 c.deps_dev = vec![Dep::simple("dev-dep", "^bad-dev")];
5781 let err = c.validate_deps().unwrap_err();
5782 assert!(
5783 matches!(
5784 err,
5785 crate::dep::DepError::VersaoInvalid { ref nome, .. }
5786 if nome == "runtime-dep"
5787 ),
5788 "expected `:deps` typo to surface first, got {err:?}"
5789 );
5790 }
5791
5792 #[test]
5793 fn validate_deps_accepts_canonical_versao_forms_in_both_lists() {
5794 // Positive control sweep across both lists. Pin every
5795 // canonical Cargo-shaped form so a future tightening of the
5796 // accepted set surfaces here as a test failure (parity with
5797 // `accepts_canonical_membro_versao_forms` and
5798 // `validate_accepts_canonical_child_versao_forms`).
5799 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5800 c.deps = vec![
5801 Dep::simple("caret", "^0.1"),
5802 Dep::simple("tilde", "~0.1.2"),
5803 Dep::simple("exact", "0.1.0"),
5804 Dep::simple("wildcard", "*"),
5805 Dep::simple("multi-range", ">=0.1, <2"),
5806 ];
5807 c.deps_dev = vec![
5808 Dep::simple("dev-caret", "^0.1"),
5809 Dep::simple("dev-wildcard", "*"),
5810 ];
5811 c.validate_deps().unwrap();
5812 }
5813
5814 #[test]
5815 fn validate_deps_diagnostic_carries_offending_dep() {
5816 // Diagnostic-shape pin: the error names the offending entry's
5817 // `:nome` + `:versao` verbatim and carries a non-empty
5818 // `reason` from `semver::VersionReq::parse`, so a `feira lint`
5819 // run can render the diagnostic without re-parsing.
5820 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5821 c.deps = vec![Dep::simple("caixa-teia", "not-a-req")];
5822 let err = c.validate_deps().unwrap_err();
5823 let crate::dep::DepError::VersaoInvalid {
5824 nome,
5825 versao,
5826 reason,
5827 } = err
5828 else {
5829 panic!("expected VersaoInvalid, got other variant");
5830 };
5831 assert_eq!(nome, "caixa-teia");
5832 assert_eq!(versao, "not-a-req");
5833 assert!(
5834 !reason.is_empty(),
5835 "VersaoInvalid `reason` must carry the parser's wording verbatim"
5836 );
5837 }
5838
5839 #[test]
5840 fn validate_deps_rejects_ambiguous_fonte_in_deps_dev() {
5841 // Cross-axis pin: `validate_deps` walks both :deps and
5842 // :deps-dev through `Dep::validate`, and the new fonte gate
5843 // (`:tag` + `:branch` both set — the canonical "pin drift"
5844 // footgun) must surface from the :deps-dev arm with the
5845 // offending entry's :nome named. Pin the :deps-dev arm
5846 // explicitly so a future shortcut that only walks :deps
5847 // surfaces here as a regression.
5848 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5849 c.deps_dev = vec![Dep {
5850 nome: "dev-only".into(),
5851 versao: "^0.1".into(),
5852 fonte: Some(crate::DepSource::Git {
5853 repo: "github:p/x".into(),
5854 tag: Some("v1".into()),
5855 rev: None,
5856 branch: Some("main".into()),
5857 }),
5858 opcional: false,
5859 caracteristicas: vec![],
5860 }];
5861 let err = c.validate_deps().unwrap_err();
5862 let crate::dep::DepError::FontePinAmbiguous { nome, pins } = err else {
5863 panic!("expected FontePinAmbiguous from :deps-dev walk");
5864 };
5865 assert_eq!(nome, "dev-only");
5866 assert!(pins.contains(":tag") && pins.contains(":branch"));
5867 }
5868
5869 #[test]
5870 fn validate_deps_rejects_empty_repo_in_deps() {
5871 // Parity pin on the :deps arm: an empty :repo on the runtime
5872 // deps list surfaces the same FonteRepoEmpty diagnostic the
5873 // dep.rs per-entry tests pin, naming the offending entry.
5874 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5875 c.deps = vec![Dep {
5876 nome: "runtime".into(),
5877 versao: "^0.1".into(),
5878 fonte: Some(crate::DepSource::Git {
5879 repo: String::new(),
5880 tag: Some("v1".into()),
5881 rev: None,
5882 branch: None,
5883 }),
5884 opcional: false,
5885 caracteristicas: vec![],
5886 }];
5887 let err = c.validate_deps().unwrap_err();
5888 assert!(
5889 matches!(
5890 err,
5891 crate::dep::DepError::FonteRepoEmpty { ref nome }
5892 if nome == "runtime"
5893 ),
5894 "got {err:?}"
5895 );
5896 }
5897
5898 // ── validate_deps: within-list :nome set-not-multiset gate ─────────
5899
5900 #[test]
5901 fn validate_deps_rejects_duplicate_nome_in_deps() {
5902 // Fail-before-pass-after pin: two `:deps` entries naming the same
5903 // caixa carry two `:versao` / `:fonte` / feature triples that the
5904 // caixa-resolver's lacre pipeline collapses (the second silently
5905 // overwrites the first at `concrete_versao`-resolve time). The
5906 // gate surfaces the duplicate at validate-time, naming the
5907 // offending caixa + the list, before the resolver-side silent
5908 // drop. Mirrors the peer typed-graph duplicate gates
5909 // (`DuplicateChildCaixa`, `MembroDuplicate`, `DuplicateFrom`, …).
5910 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5911 c.deps = vec![
5912 Dep::simple("caixa-teia", "^0.1"),
5913 Dep::simple("caixa-teia", "^0.2"),
5914 ];
5915 let err = c.validate_deps().unwrap_err();
5916 assert!(
5917 matches!(
5918 err,
5919 crate::dep::DepError::DuplicateNome { ref nome, list }
5920 if nome == "caixa-teia" && list == crate::render::DEP_AUTHOR_KEY_DEPS
5921 ),
5922 "got {err:?}"
5923 );
5924 }
5925
5926 #[test]
5927 fn validate_deps_rejects_duplicate_nome_in_deps_dev() {
5928 // Parity pin: `:deps-dev` runs through the same per-list
5929 // duplicate check as `:deps` — neither axis is a second-class
5930 // citizen of the set-not-multiset discipline.
5931 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5932 c.deps_dev = vec![
5933 Dep::simple("tatara-check", "*"),
5934 Dep::simple("tatara-check", "^0.1"),
5935 ];
5936 let err = c.validate_deps().unwrap_err();
5937 assert!(
5938 matches!(
5939 err,
5940 crate::dep::DepError::DuplicateNome { ref nome, list }
5941 if nome == "tatara-check" && list == crate::render::DEP_AUTHOR_KEY_DEPS_DEV
5942 ),
5943 "got {err:?}"
5944 );
5945 }
5946
5947 #[test]
5948 fn validate_deps_accepts_cross_list_same_nome() {
5949 // The Cargo `[dependencies]` + `[dev-dependencies]` override
5950 // convention is preserved: a name appearing in *both* lists is
5951 // valid (the dev-pin overrides at test/dev time). Only
5952 // within-list duplicates are structurally incoherent — pin the
5953 // permissive cross-list semantics so a future shortcut that
5954 // collapses the two seen-sets into one surfaces here as a test
5955 // failure.
5956 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5957 c.deps = vec![Dep::simple("caixa-teia", "^0.1")];
5958 c.deps_dev = vec![Dep::simple("caixa-teia", "^0.2")];
5959 c.validate_deps().unwrap();
5960 }
5961
5962 #[test]
5963 fn validate_deps_accepts_distinct_nome_in_both_lists() {
5964 // Positive control: distinct names within each list pass — the
5965 // gate's identity element on the canonical authoring shape.
5966 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5967 c.deps = vec![
5968 Dep::simple("caixa-teia", "^0.1"),
5969 Dep::simple("pleme-mesh", "*"),
5970 ];
5971 c.deps_dev = vec![
5972 Dep::simple("tatara-check", "*"),
5973 Dep::simple("dev-shim", "^0.1"),
5974 ];
5975 c.validate_deps().unwrap();
5976 }
5977
5978 #[test]
5979 fn validate_deps_per_entry_validate_fires_before_duplicate_in_deps() {
5980 // Diagnostic-precedence pin: a malformed `:versao` on the
5981 // duplicating entry surfaces its narrower `VersaoInvalid`
5982 // diagnostic first, before the cross-entry duplicate gate fires
5983 // — the canonical "per-entry shape before cross-entry uniqueness"
5984 // precedence every peer set-not-multiset gate establishes
5985 // (`*_invalid_fires_before_duplicate_check` pins on
5986 // `SupervisorSpec::validate`, `AplicacaoSpec::validate_membros`,
5987 // `validate_upgrade_from`).
5988 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5989 c.deps = vec![
5990 Dep::simple("caixa-teia", "^0.1"),
5991 Dep::simple("caixa-teia", "^bad-version"),
5992 ];
5993 let err = c.validate_deps().unwrap_err();
5994 assert!(
5995 matches!(
5996 err,
5997 crate::dep::DepError::VersaoInvalid { ref nome, ref versao, .. }
5998 if nome == "caixa-teia" && versao == "^bad-version"
5999 ),
6000 "expected VersaoInvalid to surface before DuplicateNome, got {err:?}"
6001 );
6002 }
6003
6004 #[test]
6005 fn validate_deps_duplicate_diagnostic_names_first_collision() {
6006 // First-collision determinism pin: with three entries naming the
6007 // same caixa, the first colliding pair surfaces — not the last.
6008 // Mirrors the peer first-collision posture on every
6009 // duplicate-target gate
6010 // (`validate_upgrade_from_duplicate_diagnostic_names_second_collision`
6011 // — the second entry is the first collision; this gate uses the
6012 // same shape: the second entry's `:nome` lands in the diagnostic
6013 // because `seen.insert(first.nome)` already populated the set).
6014 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6015 c.deps = vec![
6016 Dep::simple("caixa-teia", "^0.1"),
6017 Dep::simple("caixa-teia", "^0.2"),
6018 Dep::simple("caixa-teia", "^0.3"),
6019 ];
6020 let err = c.validate_deps().unwrap_err();
6021 // The diagnostic carries the offending caixa name; the
6022 // implementation surfaces on the *second* entry (the first
6023 // collision), so the test pins the `:nome` value.
6024 assert!(
6025 matches!(
6026 err,
6027 crate::dep::DepError::DuplicateNome { ref nome, list }
6028 if nome == "caixa-teia" && list == crate::render::DEP_AUTHOR_KEY_DEPS
6029 ),
6030 "got {err:?}"
6031 );
6032 }
6033
6034 #[test]
6035 fn validate_deps_duplicate_in_deps_fires_before_duplicate_in_deps_dev() {
6036 // Cross-list precedence pin: when both lists carry duplicates,
6037 // the `:deps` diagnostic surfaces first — same author-mental-
6038 // model ordering the `validate_deps_runs_deps_before_deps_dev`
6039 // pin establishes for malformed `:versao` (runtime axis before
6040 // dev axis).
6041 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6042 c.deps = vec![
6043 Dep::simple("runtime-dep", "^0.1"),
6044 Dep::simple("runtime-dep", "^0.2"),
6045 ];
6046 c.deps_dev = vec![Dep::simple("dev-dep", "*"), Dep::simple("dev-dep", "^0.1")];
6047 let err = c.validate_deps().unwrap_err();
6048 assert!(
6049 matches!(
6050 err,
6051 crate::dep::DepError::DuplicateNome { ref nome, list }
6052 if nome == "runtime-dep" && list == crate::render::DEP_AUTHOR_KEY_DEPS
6053 ),
6054 "expected :deps duplicate to surface before :deps-dev duplicate, got {err:?}"
6055 );
6056 }
6057
6058 #[test]
6059 fn validate_deps_empty_lists_pass_duplicate_gate() {
6060 // Empty-set identity pin: the bare template (zero deps, zero
6061 // deps_dev) passes the duplicate gate as the gate's identity
6062 // element. A future tighten that conflates "empty" with
6063 // "missing" would regress this baseline.
6064 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6065 c.validate_deps().unwrap();
6066 }
6067
6068 #[test]
6069 fn validate_deps_duplicate_diagnostic_carries_list_tag() {
6070 // Diagnostic-shape pin: the `list:` field tags which list the
6071 // duplicate landed in (`:deps` vs `:deps-dev`) verbatim, so a
6072 // `feira lint` run can route the author to the right block in
6073 // their caixa.lisp without re-deriving the list from context.
6074 // Same self-locating shape every peer per-axis diagnostic
6075 // already exposes.
6076 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6077 c.deps_dev = vec![
6078 Dep::simple("dev-thing", "*"),
6079 Dep::simple("dev-thing", "^0.1"),
6080 ];
6081 let err = c.validate_deps().unwrap_err();
6082 let crate::dep::DepError::DuplicateNome { nome, list } = err else {
6083 panic!("expected DuplicateNome from :deps-dev walk");
6084 };
6085 assert_eq!(nome, "dev-thing");
6086 assert_eq!(list, crate::render::DEP_AUTHOR_KEY_DEPS_DEV);
6087 }
6088
6089 // ── validate_deps: per-entry :caracteristicas set-discipline gate ──
6090
6091 #[test]
6092 fn validate_deps_surfaces_caracteristicas_duplicate_in_deps_list() {
6093 // Thread-through pin on `:deps`: the per-entry
6094 // `Dep::validate_caracteristicas` gate fires inside
6095 // `Caixa::validate_deps`'s linear walk, so a malformed feature
6096 // list on any `:deps` entry surfaces as a `DepError` from
6097 // `validate_deps` — the same reachability shape every per-entry
6098 // `Dep::validate` arm threads through. Without this pin a future
6099 // shortcut that skips the per-entry `Dep::validate` call on the
6100 // cross-entry-uniqueness path would mask the within-entry
6101 // `:caracteristicas` gates.
6102 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6103 c.deps = vec![Dep {
6104 nome: "caixa-teia".into(),
6105 versao: "^0.1".into(),
6106 fonte: None,
6107 opcional: false,
6108 caracteristicas: vec!["http".into(), "http".into()],
6109 }];
6110 let err = c.validate_deps().unwrap_err();
6111 let crate::dep::DepError::CaracteristicaDuplicate {
6112 nome,
6113 caracteristica,
6114 } = err
6115 else {
6116 panic!("expected CaracteristicaDuplicate from :deps walk, got {err:?}");
6117 };
6118 assert_eq!(nome, "caixa-teia");
6119 assert_eq!(caracteristica, "http");
6120 }
6121
6122 #[test]
6123 fn validate_deps_surfaces_caracteristicas_empty_in_deps_dev_list() {
6124 // Peer thread-through pin on `:deps-dev`: same reachability as
6125 // the `:deps` arm above, on the dev-only authoring axis. Pins
6126 // that the `validate_deps` walk visits both lists' per-entry
6127 // gates uniformly. The empty-feature arm carries here so both
6128 // new `:caracteristicas` arms are surfaced via at least one
6129 // `validate_deps` thread-through.
6130 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6131 c.deps_dev = vec![Dep {
6132 nome: "caixa-teia".into(),
6133 versao: "^0.1".into(),
6134 fonte: None,
6135 opcional: false,
6136 caracteristicas: vec![String::new()],
6137 }];
6138 let err = c.validate_deps().unwrap_err();
6139 let crate::dep::DepError::CaracteristicaEmpty { nome } = err else {
6140 panic!("expected CaracteristicaEmpty from :deps-dev walk, got {err:?}");
6141 };
6142 assert_eq!(nome, "caixa-teia");
6143 }
6144
6145 #[test]
6146 fn validate_deps_surfaces_caracteristicas_invalid_in_deps_list() {
6147 // Thread-through pin on `:deps`: the per-entry
6148 // `Dep::validate_caracteristicas` value-shape gate (lifted via
6149 // `crate::render::is_cargo_feature_name`) fires inside
6150 // `Caixa::validate_deps`'s linear walk on the `:deps` list, so
6151 // a structurally invalid feature name on any `:deps` entry
6152 // surfaces as `DepError::CaracteristicaInvalid` from
6153 // `validate_deps` — the same reachability shape every per-entry
6154 // `Dep::validate` arm threads through. Without this pin a
6155 // future shortcut that skips the per-entry `Dep::validate` call
6156 // on the cross-entry-uniqueness path would mask the within-
6157 // entry `:caracteristicas` value-shape gate.
6158 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6159 c.deps = vec![Dep {
6160 nome: "caixa-teia".into(),
6161 versao: "^0.1".into(),
6162 fonte: None,
6163 opcional: false,
6164 caracteristicas: vec!["+http".into()],
6165 }];
6166 let err = c.validate_deps().unwrap_err();
6167 let crate::dep::DepError::CaracteristicaInvalid {
6168 nome,
6169 caracteristica,
6170 ..
6171 } = err
6172 else {
6173 panic!("expected CaracteristicaInvalid from :deps walk, got {err:?}");
6174 };
6175 assert_eq!(nome, "caixa-teia");
6176 assert_eq!(caracteristica, "+http");
6177 }
6178
6179 #[test]
6180 fn validate_deps_surfaces_caracteristicas_invalid_in_deps_dev_list() {
6181 // Peer thread-through pin on `:deps-dev`: same reachability as
6182 // the `:deps` arm above, on the dev-only authoring axis. The
6183 // `http/json` shape carries here so the segment-separator
6184 // diagnostic (the canonical Cargo `dep/feat` namespaced-dep
6185 // confusion footgun) is surfaced via the cross-entry walk too —
6186 // pinning that the `:deps-dev` list visits the same per-entry
6187 // value-shape gate as the `:deps` list.
6188 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6189 c.deps_dev = vec![Dep {
6190 nome: "caixa-teia".into(),
6191 versao: "^0.1".into(),
6192 fonte: None,
6193 opcional: false,
6194 caracteristicas: vec!["http/json".into()],
6195 }];
6196 let err = c.validate_deps().unwrap_err();
6197 let crate::dep::DepError::CaracteristicaInvalid {
6198 nome,
6199 caracteristica,
6200 ..
6201 } = err
6202 else {
6203 panic!("expected CaracteristicaInvalid from :deps-dev walk, got {err:?}");
6204 };
6205 assert_eq!(nome, "caixa-teia");
6206 assert_eq!(caracteristica, "http/json");
6207 }
6208
6209 #[test]
6210 fn to_lisp_preserves_deps() {
6211 let src = r#"
6212(defcaixa
6213 :nome "x"
6214 :versao "0.1.0"
6215 :kind Biblioteca
6216 :deps ((:nome "a" :versao "^0.1")
6217 (:nome "b" :versao "*" :fonte (:tipo git :repo "github:o/b" :tag "v1"))))
6218"#;
6219 let c1 = Caixa::from_lisp(src).unwrap();
6220 let emitted = c1.to_lisp();
6221 let c2 = Caixa::from_lisp(&emitted).expect("round trip");
6222 assert_eq!(c1.deps, c2.deps);
6223 }
6224
6225 // ── Caixa::validate_nome — top-level :nome value-shape gate ─────────
6226
6227 fn caixa_with_nome(nome: &str) -> Caixa {
6228 let mut c = Caixa::from_lisp(&Caixa::template("placeholder")).unwrap();
6229 c.nome = nome.to_string();
6230 c
6231 }
6232
6233 #[test]
6234 fn validate_nome_accepts_canonical_template() {
6235 // Positive control: the bare `feira init`-style template's
6236 // `:nome` ("demo") is a canonical DNS-1123 label; the gate must
6237 // not regress this baseline shape. A future tightening of the
6238 // accepted set surfaces here as a test failure first.
6239 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6240 c.validate_nome().unwrap();
6241 }
6242
6243 #[test]
6244 fn validate_nome_accepts_canonical_forms() {
6245 // Positive-set sweep: each realistic caixa-name shape the K8s
6246 // apiserver accepts as a `metadata.name` label must pass —
6247 // single-word, hyphen-joined, version-suffixed, single-char,
6248 // two-char, digit-start (DNS-1123 allows this; the stricter
6249 // DNS-1035 Service-name rule doesn't), version-suffix-bearing.
6250 // Mirrors `accepts_canonical_membro_caixa_forms` (3f9d7a0) on
6251 // the peer member-name axis.
6252 for nome in [
6253 "checkout",
6254 "cart-v2",
6255 "a",
6256 "db",
6257 "3rd-party-shim",
6258 "payment-retry",
6259 "0",
6260 ] {
6261 caixa_with_nome(nome)
6262 .validate_nome()
6263 .unwrap_or_else(|e| panic!("canonical :nome {nome:?} must validate, got {e:?}"));
6264 }
6265 }
6266
6267 #[test]
6268 fn validate_nome_rejects_empty() {
6269 // Fail-before-pass-after pin: `Caixa::from_lisp` does not refuse
6270 // an empty `:nome` (the derive macro stores the raw String);
6271 // the gate's empty arm names the offending axis with a narrower
6272 // diagnostic than the `NomeInvalid` parse arm would emit.
6273 let c = caixa_with_nome("");
6274 let err = c.validate_nome().unwrap_err();
6275 assert_eq!(err, ManifestError::NomeEmpty);
6276 }
6277
6278 #[test]
6279 fn validate_nome_rejects_uppercase() {
6280 // The canonical "I copied the TitleCase display name verbatim"
6281 // footgun. The K8s apiserver rejects `metadata.name: MyApp` at
6282 // admission on every derived artifact (Helm chart, ComputeUnit,
6283 // CNP, HTTPRoute, label values); the gate moves the diagnostic
6284 // to the source `caixa.lisp` and the reason suggests the
6285 // lowercased fix verbatim.
6286 let c = caixa_with_nome("MyApp");
6287 let err = c.validate_nome().unwrap_err();
6288 let ManifestError::NomeInvalid { nome, reason } = err else {
6289 panic!("expected NomeInvalid for uppercase :nome");
6290 };
6291 assert_eq!(nome, "MyApp");
6292 assert!(
6293 reason.contains("uppercase") && reason.contains("myapp"),
6294 "diagnostic must name the violation + the lowercased fix, got {reason:?}"
6295 );
6296 }
6297
6298 #[test]
6299 fn validate_nome_rejects_underscore() {
6300 // The Python-/Postgres-style `snake_case` leak. DNS-1123 forbids
6301 // `_`; the apiserver rejects on admission across every derived
6302 // artifact. Same fixture pinned for `:membros :caixa` (3f9d7a0)
6303 // and `:children :caixa` (31bfa43).
6304 let c = caixa_with_nome("my_app");
6305 let err = c.validate_nome().unwrap_err();
6306 assert!(
6307 matches!(
6308 err,
6309 ManifestError::NomeInvalid { ref nome, ref reason }
6310 if nome == "my_app" && reason.contains('_')
6311 ),
6312 "got {err:?}"
6313 );
6314 }
6315
6316 #[test]
6317 fn validate_nome_rejects_dot() {
6318 // A `:nome` is a single DNS-1123 label, not a subdomain. The
6319 // "I want to namespace with `.`" footgun the gate redirects to
6320 // `-` via the shared predicate's reason wording.
6321 let c = caixa_with_nome("team.app");
6322 let err = c.validate_nome().unwrap_err();
6323 assert!(
6324 matches!(
6325 err,
6326 ManifestError::NomeInvalid { ref nome, ref reason }
6327 if nome == "team.app" && reason.contains('.')
6328 ),
6329 "got {err:?}"
6330 );
6331 }
6332
6333 #[test]
6334 fn validate_nome_rejects_leading_hyphen() {
6335 // DNS-1123 boundary rule: the label must start with an ASCII
6336 // alphanumeric. Pin the leading-`-` arm explicitly.
6337 let c = caixa_with_nome("-app");
6338 let err = c.validate_nome().unwrap_err();
6339 assert!(
6340 matches!(
6341 err,
6342 ManifestError::NomeInvalid { ref nome, .. } if nome == "-app"
6343 ),
6344 "got {err:?}"
6345 );
6346 }
6347
6348 #[test]
6349 fn validate_nome_rejects_trailing_hyphen() {
6350 // Symmetric arm of the boundary rule, pinned separately so a
6351 // future relaxation that only checks the leading position
6352 // surfaces here. Mirrors `rejects_membro_caixa_with_trailing_hyphen`
6353 // and `_with_trailing_hyphen` on the supervisor / aplicacao
6354 // axes.
6355 let c = caixa_with_nome("app-");
6356 let err = c.validate_nome().unwrap_err();
6357 assert!(
6358 matches!(
6359 err,
6360 ManifestError::NomeInvalid { ref nome, .. } if nome == "app-"
6361 ),
6362 "got {err:?}"
6363 );
6364 }
6365
6366 #[test]
6367 fn validate_nome_rejects_unicode() {
6368 // IDN must be pre-encoded as Punycode (`xn--…`); raw Unicode
6369 // bytes are rejected by the K8s apiserver on every name axis.
6370 let c = caixa_with_nome("café");
6371 let err = c.validate_nome().unwrap_err();
6372 assert!(
6373 matches!(
6374 err,
6375 ManifestError::NomeInvalid { ref nome, .. } if nome == "café"
6376 ),
6377 "got {err:?}"
6378 );
6379 }
6380
6381 #[test]
6382 fn validate_nome_rejects_whitespace() {
6383 // The paste-from-sketch / paste-from-spec footgun. Internal
6384 // whitespace is rejected by every K8s name axis.
6385 let c = caixa_with_nome("my app");
6386 let err = c.validate_nome().unwrap_err();
6387 assert!(
6388 matches!(
6389 err,
6390 ManifestError::NomeInvalid { ref nome, .. } if nome == "my app"
6391 ),
6392 "got {err:?}"
6393 );
6394 }
6395
6396 #[test]
6397 fn validate_nome_rejects_too_long() {
6398 // 64-byte boundary pin: the K8s apiserver rejects any
6399 // `metadata.name` over 63 bytes at admission; the diagnostic
6400 // names both the 63-byte cap and the actual length so the
6401 // author can shorten in one edit. Mirrors `_too_long` on the
6402 // peer member-/cluster-/child-name axes.
6403 let over = "a".repeat(crate::DNS_1123_LABEL_MAX_LEN + 1);
6404 let c = caixa_with_nome(&over);
6405 let err = c.validate_nome().unwrap_err();
6406 let ManifestError::NomeInvalid { nome, reason } = err else {
6407 panic!("expected NomeInvalid for over-cap :nome");
6408 };
6409 assert_eq!(nome.len(), crate::DNS_1123_LABEL_MAX_LEN + 1);
6410 assert!(
6411 reason.contains("63") && reason.contains("64"),
6412 "diagnostic must name the cap + actual length, got {reason:?}"
6413 );
6414 }
6415
6416 #[test]
6417 fn nome_max_length_validates() {
6418 // The 63-byte cap exactly — the boundary-accepting case pinned
6419 // alongside `validate_nome_rejects_too_long` so a future cap
6420 // shift surfaces both arms simultaneously. Mirrors
6421 // `membro_caixa_max_length_validates`,
6422 // `placement_cluster_max_length_validates`,
6423 // `child_caixa_max_length_validates`.
6424 let at_cap = "a".repeat(crate::DNS_1123_LABEL_MAX_LEN);
6425 caixa_with_nome(&at_cap).validate_nome().unwrap();
6426 }
6427
6428 #[test]
6429 fn nome_empty_takes_precedence_over_invalid() {
6430 // Order pin: the empty arm fires before the predicate is
6431 // consulted. Empty < invalid in self-locating-ness — the
6432 // narrower `NomeEmpty` diagnostic doesn't carry a useless
6433 // `nome: ""` reference into the parser-shaped reason. Mirrors
6434 // `membro_caixa_empty_takes_precedence_over_invalid` on the
6435 // peer axis (3f9d7a0).
6436 let c = caixa_with_nome("");
6437 assert_eq!(c.validate_nome().unwrap_err(), ManifestError::NomeEmpty);
6438 }
6439
6440 #[test]
6441 fn nome_invalid_diagnostic_carries_offending_nome() {
6442 // Diagnostic-shape pin: the error names the offending `:nome`
6443 // verbatim with a non-empty parser-shaped reason, so a `feira
6444 // lint` run can render the diagnostic without re-parsing.
6445 // Mirrors `membro_caixa_invalid_diagnostic_carries_offending_caixa`.
6446 let c = caixa_with_nome("MyApp");
6447 let err = c.validate_nome().unwrap_err();
6448 let ManifestError::NomeInvalid { nome, reason } = err else {
6449 panic!("expected NomeInvalid variant");
6450 };
6451 assert_eq!(nome, "MyApp");
6452 assert!(
6453 !reason.is_empty(),
6454 "NomeInvalid `reason` must carry the predicate's wording verbatim"
6455 );
6456 }
6457
6458 // ── Caixa::validate_nome_chart_name_budget — joint-length on `:nome` ──
6459 //
6460 // The bare-`:nome` axis [`Caixa::validate_nome`] caps at 63 bytes
6461 // via DNS-1123; this second-axis gate caps the joint
6462 // `lareira-<nome>` chart name at the same 63-byte ceiling. The
6463 // canonical [`crate::lareira_chart_name`] helper's doc comment
6464 // (f7320d7, caixa-core/src/render.rs:3198) explicitly deferred:
6465 // "the M4 admission webhook will pin the joint-length invariant
6466 // when it lands". These tests pin it at the manifest-validate
6467 // layer instead, fail-before-pass-after on the 56-byte boundary.
6468
6469 #[test]
6470 fn validate_nome_chart_name_budget_accepts_canonical_template() {
6471 // Positive control: the bare `feira init`-style template's
6472 // `:nome` ("demo") sits far below the cap; the gate must not
6473 // regress this baseline. Same shape every peer
6474 // value-shape-gate baseline pin uses.
6475 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6476 c.validate_nome_chart_name_budget().unwrap();
6477 }
6478
6479 #[test]
6480 fn validate_nome_chart_name_budget_accepts_canonical_fixtures() {
6481 // Positive-set sweep across the canonical author surface every
6482 // in-tree fixture uses (`hello-rio`, `cart`, `checkout`,
6483 // `worker`, the `checkout-aplicacao` example members, the
6484 // `akeyless-attest` caixa-tatara fixture). Every value sits
6485 // far below the 55-byte per-`:nome` budget. Same shape every
6486 // peer per-axis baseline pin uses.
6487 for nome in [
6488 "hello-rio",
6489 "cart",
6490 "checkout",
6491 "worker",
6492 "akeyless-attest",
6493 "demo",
6494 "a",
6495 ] {
6496 caixa_with_nome(nome)
6497 .validate_nome_chart_name_budget()
6498 .unwrap_or_else(|e| {
6499 panic!("canonical :nome {nome:?} must pass chart-name budget, got {e:?}")
6500 });
6501 }
6502 }
6503
6504 #[test]
6505 fn validate_nome_chart_name_budget_accepts_nome_at_cap() {
6506 // Boundary-accepting case at the 55-byte per-`:nome` budget —
6507 // the joint chart name is exactly 63 bytes, the DNS-1123 label
6508 // cap. Pinned alongside the rejecting-arm test so a future cap
6509 // shift surfaces both arms simultaneously. Mirrors
6510 // `nome_max_length_validates` on the peer bare-`:nome` axis.
6511 let at_cap = "a".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN);
6512 caixa_with_nome(&at_cap)
6513 .validate_nome_chart_name_budget()
6514 .unwrap();
6515 }
6516
6517 #[test]
6518 fn validate_nome_chart_name_budget_rejects_nome_one_over_cap() {
6519 // Fail-before-pass-after pin on the 56-byte boundary: the
6520 // smallest `:nome` length that overflows the joint chart-name
6521 // cap. The inner [`is_dns_1123_label`] gate
6522 // (`Caixa::validate_nome`) accepts it (56 ≤ 63), so prior to
6523 // this gate it silently passed the manifest-validate cascade
6524 // and surfaced as a `helm lint` / apiserver rejection on the
6525 // rendered chart name far from the source `caixa.lisp`, with
6526 // no field naming the overflow. With this gate the diagnostic
6527 // names the offending `:nome` verbatim alongside the rendered
6528 // chart name and the budget, so the author can shorten in one
6529 // edit. Mirrors `validate_nome_rejects_too_long` on the peer
6530 // bare-`:nome` axis.
6531 let over = "a".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 1);
6532 let c = caixa_with_nome(&over);
6533 let err = c.validate_nome_chart_name_budget().unwrap_err();
6534 let ManifestError::NomeChartNameBudgetExceeded { nome, reason } = err else {
6535 panic!("expected NomeChartNameBudgetExceeded for over-budget :nome");
6536 };
6537 assert_eq!(nome.len(), crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 1);
6538 assert_eq!(nome, over);
6539 assert!(
6540 reason.contains("63") && reason.contains("64") && reason.contains("55"),
6541 "diagnostic must name the DNS-1123 cap (63), the actual chart-name length (64), \
6542 and the per-`:nome` budget (55), got {reason:?}"
6543 );
6544 }
6545
6546 #[test]
6547 fn validate_nome_chart_name_budget_rejects_nome_at_bare_dns_cap() {
6548 // The 63-byte `:nome` boundary — passes the bare-`:nome`
6549 // [`is_dns_1123_label`] cap exactly, but produces a 71-byte
6550 // joint chart name that overflows the DNS-1123 label cap
6551 // structurally. The most stringent fail-before-pass-after
6552 // surface: every `:nome` in the 56..=63-byte range passed the
6553 // prior cascade and broke at admission.
6554 let bare_max = "a".repeat(crate::DNS_1123_LABEL_MAX_LEN);
6555 let c = caixa_with_nome(&bare_max);
6556 // The bare-`:nome` gate accepts the 63-byte length.
6557 c.validate_nome().unwrap();
6558 // The new joint-length gate rejects it.
6559 let err = c.validate_nome_chart_name_budget().unwrap_err();
6560 assert!(
6561 matches!(
6562 err,
6563 ManifestError::NomeChartNameBudgetExceeded { ref nome, .. }
6564 if nome.len() == crate::DNS_1123_LABEL_MAX_LEN
6565 ),
6566 "got {err:?}"
6567 );
6568 }
6569
6570 #[test]
6571 fn validate_nome_chart_name_budget_diagnostic_carries_offending_chart_name() {
6572 // Diagnostic-shape pin: the rendered `lareira-<nome>` chart
6573 // name appears verbatim in the diagnostic so the author sees
6574 // exactly the string the apiserver / `helm lint` would have
6575 // rejected — no re-derivation required to grep the source.
6576 // Peer with `nome_invalid_diagnostic_carries_offending_nome`
6577 // on the bare-`:nome` axis.
6578 let over = "x".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 5);
6579 let c = caixa_with_nome(&over);
6580 let err = c.validate_nome_chart_name_budget().unwrap_err();
6581 let ManifestError::NomeChartNameBudgetExceeded { nome, reason } = err else {
6582 panic!("expected NomeChartNameBudgetExceeded variant");
6583 };
6584 assert_eq!(nome, over);
6585 let expected_chart = crate::lareira_chart_name(&over);
6586 assert!(
6587 reason.contains(&expected_chart),
6588 "diagnostic must carry the rendered chart name {expected_chart:?} verbatim, \
6589 got {reason:?}"
6590 );
6591 assert!(
6592 reason.contains("lareira-"),
6593 "diagnostic must name the canonical chart-name prefix verbatim, got {reason:?}"
6594 );
6595 }
6596
6597 #[test]
6598 fn validate_nome_chart_name_budget_runs_after_nome_shape_via_layout_verify() {
6599 // Order pin on the layout cascade: the narrower
6600 // `NomeInvalid` (bare-DNS-1123 shape) fires before the
6601 // joint-length budget. A structurally-malformed `:nome` (here:
6602 // uppercase) surfaces its specific shape error rather than
6603 // the chart-name-budget error, even when the joint length
6604 // would also overflow — the narrower diagnostic is more
6605 // self-locating. Mirrors the cascade-precedence pins peer
6606 // gates already use (e.g. `EntradaParaEmpty` before
6607 // `EntradaParaInvalid`).
6608 let over = "A".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 1);
6609 let c = caixa_with_nome(&over);
6610 // The bare-shape gate fires first.
6611 let err = c.validate_nome().unwrap_err();
6612 assert!(
6613 matches!(err, ManifestError::NomeInvalid { .. }),
6614 "bare-shape gate must fire before chart-name-budget gate; got {err:?}"
6615 );
6616 // And the layout verify cascade surfaces that diagnostic, not
6617 // the budget arm. Inject a path-exists oracle so the cascade
6618 // gets past the manifest-presence check and into the
6619 // value-shape gates.
6620 let layout = crate::StandardLayout::new().with_path_exists(|_| true);
6621 let err = crate::LayoutInvariants::verify(
6622 &layout,
6623 &c,
6624 std::path::Path::new("/tmp/caixa-test-fake-root"),
6625 )
6626 .unwrap_err();
6627 let issue = err.to_string();
6628 assert!(
6629 issue.contains("DNS-1123") || issue.contains("uppercase"),
6630 "layout cascade must surface the bare-DNS-1123 diagnostic on a \
6631 structurally-malformed :nome, not the chart-name-budget diagnostic; got {issue:?}"
6632 );
6633 }
6634
6635 #[test]
6636 fn layout_verify_routes_chart_name_budget_through_nome_violation() {
6637 // Cross-axis envelope pin: the layout cascade wraps both
6638 // bare-`:nome` and joint-length-`:nome` failures through the
6639 // same [`LayoutError::NomeViolation`] envelope, since both
6640 // arms are on the `:nome` axis. The user's diagnostic stays
6641 // self-locating ("which axis"), and a future consumer that
6642 // dispatches on the layout-error variant (e.g. a `feira lint`
6643 // exit-code mapping) sees a single per-axis envelope. The
6644 // wrapped `issue:` carries the full inner diagnostic.
6645 let over = "a".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 1);
6646 let c = caixa_with_nome(&over);
6647 // The bare-shape gate accepts.
6648 c.validate_nome().unwrap();
6649 let layout = crate::StandardLayout::new().with_path_exists(|_| true);
6650 let err = crate::LayoutInvariants::verify(
6651 &layout,
6652 &c,
6653 std::path::Path::new("/tmp/caixa-test-fake-root"),
6654 )
6655 .unwrap_err();
6656 let crate::LayoutError::NomeViolation { caixa, issue } = err else {
6657 panic!("expected LayoutError::NomeViolation, got {err:?}");
6658 };
6659 assert_eq!(caixa, over);
6660 assert!(
6661 issue.contains("lareira-") && issue.contains("63") && issue.contains("55"),
6662 "wrapped issue must carry the joint-length diagnostic verbatim, got {issue:?}"
6663 );
6664 }
6665
6666 // ── Caixa::validate_versao — top-level :versao value-shape gate ─────
6667
6668 fn caixa_with_versao(versao: &str) -> Caixa {
6669 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6670 c.versao = versao.to_string();
6671 c
6672 }
6673
6674 #[test]
6675 fn validate_versao_accepts_canonical_template() {
6676 // Positive control: the bare `feira init`-style template's
6677 // `:versao` ("0.1.0") is a canonical SemVer-2 literal; the gate
6678 // must not regress this baseline shape. A future tightening of
6679 // the accepted set surfaces here as a test failure first.
6680 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6681 c.validate_versao().unwrap();
6682 }
6683
6684 #[test]
6685 fn validate_versao_accepts_canonical_forms() {
6686 // Positive-set sweep: each realistic SemVer-2 shape the
6687 // substrate's downstream consumers accept must pass — bare
6688 // MAJOR.MINOR.PATCH, pre-release tags (`-rc.1`, `-alpha.0`),
6689 // build metadata (`+build.42`), the combined form, and the
6690 // `0.0.0` boundary case. Mirrors `accepts_canonical_forms` on
6691 // the peer `:nome` axis (6c992f8).
6692 for versao in [
6693 "0.1.0",
6694 "0.0.0",
6695 "1.0.0",
6696 "0.2.0-rc.1",
6697 "1.0.0-alpha.0",
6698 "1.0.0+build.42",
6699 "1.0.0-rc.1+build.42",
6700 "10.20.30",
6701 ] {
6702 caixa_with_versao(versao)
6703 .validate_versao()
6704 .unwrap_or_else(|e| {
6705 panic!("canonical :versao {versao:?} must validate, got {e:?}")
6706 });
6707 }
6708 }
6709
6710 #[test]
6711 fn validate_versao_rejects_empty() {
6712 // Fail-before-pass-after pin: `Caixa::from_lisp` does not refuse
6713 // an empty `:versao` (the derive macro stores the raw String);
6714 // the gate's empty arm names the offending axis with a narrower
6715 // diagnostic than the `VersaoInvalid` parse arm would emit.
6716 // Mirrors `validate_nome_rejects_empty` (6c992f8).
6717 let c = caixa_with_versao("");
6718 let err = c.validate_versao().unwrap_err();
6719 assert_eq!(err, ManifestError::VersaoEmpty);
6720 }
6721
6722 #[test]
6723 fn validate_versao_rejects_git_tag_shape() {
6724 // The canonical "I copied the git tag verbatim" footgun —
6725 // `feira publish` *emits* `v<versao>` git tags, so a leaked
6726 // `v0.1.0` in `:versao` would render as `vv0.1.0` and silently
6727 // shift every downstream consumer's version axis. `semver`
6728 // rejects the leading `v` at parse time; the gate moves the
6729 // diagnostic to the source `caixa.lisp`.
6730 let c = caixa_with_versao("v0.1.0");
6731 let err = c.validate_versao().unwrap_err();
6732 let ManifestError::VersaoInvalid { versao, reason } = err else {
6733 panic!("expected VersaoInvalid for git-tag-shape :versao");
6734 };
6735 assert_eq!(versao, "v0.1.0");
6736 assert!(
6737 !reason.is_empty(),
6738 "VersaoInvalid `reason` must carry the parser's wording, got {reason:?}"
6739 );
6740 }
6741
6742 #[test]
6743 fn validate_versao_rejects_missing_patch() {
6744 // The canonical "I shortened it" footgun — SemVer-2 requires
6745 // three parts. Cargo's `version =` field accepts the shortened
6746 // form as a requirement, conflating the two leaks across the
6747 // typed `:deps :versao` vs top-level `:versao` axes; the gate
6748 // pins the top-level axis to the strict three-part shape.
6749 let c = caixa_with_versao("0.1");
6750 let err = c.validate_versao().unwrap_err();
6751 assert!(
6752 matches!(
6753 err,
6754 ManifestError::VersaoInvalid { ref versao, .. } if versao == "0.1"
6755 ),
6756 "got {err:?}"
6757 );
6758 }
6759
6760 #[test]
6761 fn validate_versao_rejects_requirement_shape() {
6762 // The canonical "I leaked a requirement into a version" footgun —
6763 // the typed `:deps :versao` / `:membros :versao` axes accept
6764 // `^0.1` (a `VersionReq`); the top-level `:versao` requires a
6765 // concrete `Version`. Without this gate the two typed surfaces
6766 // would silently overlap, and a top-level `^0.1` would surface
6767 // at `helm install` time as a Chart.yaml version rejection far
6768 // from the source `caixa.lisp`.
6769 let c = caixa_with_versao("^0.1");
6770 let err = c.validate_versao().unwrap_err();
6771 assert!(
6772 matches!(
6773 err,
6774 ManifestError::VersaoInvalid { ref versao, .. } if versao == "^0.1"
6775 ),
6776 "got {err:?}"
6777 );
6778 }
6779
6780 #[test]
6781 fn validate_versao_rejects_docker_tag_shape() {
6782 // The "I confused it with a docker tag" footgun — `latest`,
6783 // `main`, `stable` parse as identifiers, not SemVer-2 versions.
6784 // SemVer rejects at parse time; the gate moves the diagnostic
6785 // to the source `caixa.lisp`.
6786 for bad in ["latest", "main", "stable"] {
6787 let c = caixa_with_versao(bad);
6788 let err = c.validate_versao().unwrap_err();
6789 assert!(
6790 matches!(
6791 err,
6792 ManifestError::VersaoInvalid { ref versao, .. } if versao == bad
6793 ),
6794 "got {err:?} for {bad:?}"
6795 );
6796 }
6797 }
6798
6799 #[test]
6800 fn validate_versao_rejects_four_part_form() {
6801 // The Java/Microsoft "MAJOR.MINOR.PATCH.BUILD" convention
6802 // SemVer-2 forbids. A leak from a non-SemVer ecosystem; the
6803 // semver crate rejects the extra `.0` at parse time.
6804 let c = caixa_with_versao("0.1.0.0");
6805 let err = c.validate_versao().unwrap_err();
6806 assert!(
6807 matches!(
6808 err,
6809 ManifestError::VersaoInvalid { ref versao, .. } if versao == "0.1.0.0"
6810 ),
6811 "got {err:?}"
6812 );
6813 }
6814
6815 #[test]
6816 fn versao_empty_takes_precedence_over_invalid() {
6817 // Order pin: the empty arm fires before the parser is consulted.
6818 // Empty < invalid in self-locating-ness — the narrower
6819 // `VersaoEmpty` diagnostic doesn't carry a useless `versao: ""`
6820 // reference into the parser-shaped reason. Mirrors
6821 // `nome_empty_takes_precedence_over_invalid` (6c992f8) on the
6822 // peer axis.
6823 let c = caixa_with_versao("");
6824 assert_eq!(c.validate_versao().unwrap_err(), ManifestError::VersaoEmpty);
6825 }
6826
6827 #[test]
6828 fn versao_invalid_diagnostic_carries_offending_versao() {
6829 // Diagnostic-shape pin: the error names the offending `:versao`
6830 // verbatim with a non-empty parser-shaped reason, so a `feira
6831 // lint` run can render the diagnostic without re-parsing.
6832 // Mirrors `nome_invalid_diagnostic_carries_offending_nome`.
6833 let c = caixa_with_versao("v0.1.0");
6834 let err = c.validate_versao().unwrap_err();
6835 let ManifestError::VersaoInvalid { versao, reason } = err else {
6836 panic!("expected VersaoInvalid variant");
6837 };
6838 assert_eq!(versao, "v0.1.0");
6839 assert!(
6840 !reason.is_empty(),
6841 "VersaoInvalid `reason` must carry the parser's wording verbatim"
6842 );
6843 }
6844
6845 #[test]
6846 fn validate_versao_accepts_what_upgrade_from_from_accepts() {
6847 // Parity pin: every shape `UpgradeFromEntry::validate` accepts
6848 // for `:upgrade-from :from` must also pass `validate_versao` —
6849 // the two `:versao`-typed surfaces (top-level `:versao`,
6850 // `:upgrade-from :from`) consume the *same* `semver::Version`
6851 // parser, so they must agree on the accepted set. Without this
6852 // pin, a future tightening of one axis could silently diverge
6853 // from the other. Mirrors the `:versao` requirement-axis
6854 // parity (`:deps`/`:deps-dev`/`:membros`/`:children`) the prior
6855 // commits established.
6856 for versao in ["0.1.0", "0.2.0-rc.1", "1.0.0+build.42"] {
6857 // From the canonical UpgradeFromEntry round-trip fixture
6858 // (`upgrade::tests::round_trip_load_module` peers).
6859 let entry = crate::UpgradeFromEntry {
6860 from: versao.to_string(),
6861 instructions: Vec::new(),
6862 };
6863 entry
6864 .validate()
6865 .unwrap_or_else(|e| panic!(":from {versao:?} must validate, got {e:?}"));
6866 caixa_with_versao(versao)
6867 .validate_versao()
6868 .unwrap_or_else(|e| {
6869 panic!(":versao {versao:?} must validate, got {e:?} — peer axis diverges")
6870 });
6871 }
6872 }
6873
6874 // ── Caixa::validate_restart_window — supervisor restart-window
6875 // folds through the shared `supervisor::duration_codec` ────────
6876
6877 fn caixa_with_restart_window(window: Option<&str>) -> Caixa {
6878 let mut c = Caixa::from_lisp(&Caixa::template("root")).unwrap();
6879 c.kind = CaixaKind::Supervisor;
6880 c.restart_window = window.map(str::to_string);
6881 c
6882 }
6883
6884 #[test]
6885 fn validate_restart_window_accepts_none() {
6886 // The canonical "omit the slot to express no reset" shape — a
6887 // `None` raw string is the absence of the typed
6888 // `:restart-window` slot, which is exactly the SupervisorSpec
6889 // "never reset" semantics. The gate must be a no-op here; a
6890 // future tightening that rejected `None` would force every
6891 // supervisor caixa to authoring-time pin a window even when
6892 // the OTP semantics call for none.
6893 caixa_with_restart_window(None)
6894 .validate_restart_window()
6895 .unwrap();
6896 }
6897
6898 #[test]
6899 fn validate_restart_window_accepts_canonical_forms() {
6900 // Positive-set sweep across the canonical authoring units the
6901 // shared `supervisor::duration_codec::parse` accepts —
6902 // matches the codec-side `parse_accepts_integer_canonical_units`
6903 // pin in supervisor::tests so a future codec-side tightening
6904 // surfaces simultaneously on both axes.
6905 for window in ["60s", "5m", "1h", "500ms", "30", "0s"] {
6906 caixa_with_restart_window(Some(window))
6907 .validate_restart_window()
6908 .unwrap_or_else(|e| {
6909 panic!("canonical :restart-window {window:?} must validate, got {e:?}")
6910 });
6911 }
6912 }
6913
6914 #[test]
6915 fn validate_restart_window_rejects_fractional_seconds() {
6916 // Fail-before-pass-after pin: the `"1.5s"` drift class (parses
6917 // as f64 to 1.5 → renders back as `"1500ms"` on first
6918 // serialize). Prior to the fold + this gate, the inline
6919 // `parse_window_inline` accepted f64 magnitudes and silently
6920 // produced a `Duration::from_secs_f64(1.5)`, divergent from
6921 // the shared codec's integer-magnitude discipline on the
6922 // serde-routed siblings. The gate now surfaces a self-locating
6923 // diagnostic at the manifest layer.
6924 let err = caixa_with_restart_window(Some("1.5s"))
6925 .validate_restart_window()
6926 .unwrap_err();
6927 let ManifestError::RestartWindowMalformed {
6928 restart_window,
6929 reason,
6930 } = err
6931 else {
6932 panic!("expected RestartWindowMalformed for fractional seconds");
6933 };
6934 assert_eq!(restart_window, "1.5s");
6935 assert!(
6936 reason.contains("\"1.5\"") && reason.contains("not a non-negative integer"),
6937 "diagnostic must carry shared-codec wording, got {reason:?}"
6938 );
6939 }
6940
6941 #[test]
6942 fn validate_restart_window_rejects_decimal_shaped_integer() {
6943 // The `"1.0s"` class — numerically `1s` exactly, but the
6944 // canonical form is `"1s"` not `"1.0s"`. Decimal-shape leak
6945 // gets the same canonical-form diagnostic.
6946 let err = caixa_with_restart_window(Some("1.0s"))
6947 .validate_restart_window()
6948 .unwrap_err();
6949 assert!(
6950 matches!(
6951 err,
6952 ManifestError::RestartWindowMalformed { ref restart_window, .. }
6953 if restart_window == "1.0s"
6954 ),
6955 "got {err:?}"
6956 );
6957 }
6958
6959 #[test]
6960 fn validate_restart_window_rejects_half_unit_minute() {
6961 // `"0.5m"` is the unit-fraction footgun — author writes a
6962 // human-readable half-minute, the prior inline parser silently
6963 // produced `Duration::from_secs_f64(30.0)` and serde
6964 // re-emitted as `"30s"`, rewriting author intent. The gate
6965 // closes the loop at the manifest layer.
6966 let err = caixa_with_restart_window(Some("0.5m"))
6967 .validate_restart_window()
6968 .unwrap_err();
6969 let ManifestError::RestartWindowMalformed {
6970 restart_window,
6971 reason,
6972 } = err
6973 else {
6974 panic!("expected RestartWindowMalformed");
6975 };
6976 assert_eq!(restart_window, "0.5m");
6977 assert!(
6978 reason.contains("\"30s\""),
6979 "diagnostic must point at the canonical-form remediation, got {reason:?}"
6980 );
6981 }
6982
6983 #[test]
6984 fn validate_restart_window_rejects_leading_sign() {
6985 // `"+30s"` and `"-30s"` both round-tripped through f64 cleanly
6986 // on the prior parser (`+30` parses as `30.0`; `-30` parsed
6987 // and was caught by the `num < 0.0` arm which silently
6988 // returned `None`, dropping the author-supplied window). The
6989 // shared codec's digit-only gate rejects both with a unified
6990 // canonical-form diagnostic; the manifest-layer wrapper names
6991 // the offending value.
6992 for bad in ["+30s", "-30s"] {
6993 let err = caixa_with_restart_window(Some(bad))
6994 .validate_restart_window()
6995 .unwrap_err();
6996 assert!(
6997 matches!(
6998 err,
6999 ManifestError::RestartWindowMalformed { ref restart_window, .. }
7000 if restart_window == bad
7001 ),
7002 "got {err:?} for {bad:?}"
7003 );
7004 }
7005 }
7006
7007 #[test]
7008 fn validate_restart_window_rejects_unknown_unit() {
7009 // `"30x"` — the typo / wrong-unit footgun. The shared codec's
7010 // unit dispatch surfaces an `unknown duration unit` reason;
7011 // the manifest-layer wrapper names the offending value.
7012 let err = caixa_with_restart_window(Some("30x"))
7013 .validate_restart_window()
7014 .unwrap_err();
7015 let ManifestError::RestartWindowMalformed {
7016 restart_window,
7017 reason,
7018 } = err
7019 else {
7020 panic!("expected RestartWindowMalformed for unknown unit");
7021 };
7022 assert_eq!(restart_window, "30x");
7023 assert!(
7024 reason.contains("unknown duration unit"),
7025 "diagnostic must carry shared-codec unit-rejection wording, got {reason:?}"
7026 );
7027 }
7028
7029 #[test]
7030 fn validate_restart_window_rejects_garbage() {
7031 // Pure non-numeric magnitude (`"abc"`) falls through to the
7032 // shared codec's narrower `"bad duration magnitude"` arm. Same
7033 // diagnostic shape as the codec-side
7034 // `parse_garbage_still_falls_through_to_bad_magnitude` pin.
7035 let err = caixa_with_restart_window(Some("abc"))
7036 .validate_restart_window()
7037 .unwrap_err();
7038 let ManifestError::RestartWindowMalformed {
7039 restart_window,
7040 reason,
7041 } = err
7042 else {
7043 panic!("expected RestartWindowMalformed for garbage");
7044 };
7045 assert_eq!(restart_window, "abc");
7046 assert!(
7047 reason.contains("bad duration magnitude"),
7048 "diagnostic must carry shared-codec garbage-rejection wording, got {reason:?}"
7049 );
7050 }
7051
7052 #[test]
7053 fn validate_restart_window_rejects_empty_string() {
7054 // The empty-after-trim edge case — distinct from the `None`
7055 // canonical "omit the slot" shape. The shared codec's
7056 // digit-only gate refuses an empty magnitude; the manifest
7057 // layer names the offending `""` so the author can grep for
7058 // the literal empty value in their `caixa.lisp` and either
7059 // remove the slot (the canonical "no reset" shape) or pin a
7060 // positive duration.
7061 let err = caixa_with_restart_window(Some(""))
7062 .validate_restart_window()
7063 .unwrap_err();
7064 assert!(
7065 matches!(
7066 err,
7067 ManifestError::RestartWindowMalformed { ref restart_window, .. }
7068 if restart_window.is_empty()
7069 ),
7070 "got {err:?}"
7071 );
7072 }
7073
7074 #[test]
7075 fn validate_restart_window_diagnostic_carries_offending_value() {
7076 // Diagnostic-shape pin (peer with
7077 // `nome_invalid_diagnostic_carries_offending_nome` /
7078 // `versao_invalid_diagnostic_carries_offending_versao`): the
7079 // error names the offending raw `:restart-window` verbatim
7080 // with a non-empty shared-codec-shaped reason, so a `feira
7081 // lint` run can render the diagnostic without re-parsing.
7082 let err = caixa_with_restart_window(Some("1.5s"))
7083 .validate_restart_window()
7084 .unwrap_err();
7085 let ManifestError::RestartWindowMalformed {
7086 restart_window,
7087 reason,
7088 } = err
7089 else {
7090 panic!("expected RestartWindowMalformed variant");
7091 };
7092 assert_eq!(restart_window, "1.5s");
7093 assert!(
7094 !reason.is_empty(),
7095 "RestartWindowMalformed `reason` must carry the codec's wording verbatim"
7096 );
7097 }
7098
7099 #[test]
7100 fn supervisor_view_folds_through_shared_codec_on_canonical_form() {
7101 // Behavioral parity pin after the fold (`parse_window_inline`
7102 // deletion): the canonical `"60s"` still produces
7103 // `Duration::from_secs(60)` on the typed view — the fold is
7104 // semantically equivalent to the prior inline parser on the
7105 // accepted set. Mirrors the pre-fold `supervisor_view_returns_typed_shape`
7106 // pin, narrowed to the parser-side contract.
7107 let c = caixa_with_restart_window(Some("60s"));
7108 let view = c.supervisor_view().expect("Supervisor kind has a view");
7109 assert_eq!(
7110 view.restart_window,
7111 Some(std::time::Duration::from_secs(60))
7112 );
7113 }
7114
7115 #[test]
7116 fn supervisor_view_soft_swallows_what_validate_rejects() {
7117 // Parity pin between the view-construction path and the
7118 // manifest-level validator: the same `"1.5s"` that surfaces
7119 // `RestartWindowMalformed` at `validate_restart_window` time
7120 // becomes `restart_window: None` on the typed view (the fold
7121 // preserves the existing best-effort shape of `supervisor_view`).
7122 // The contract is: a layout-verifier / `feira lint` flow that
7123 // cares about the malformed-window axis MUST consult
7124 // `validate_restart_window` — relying solely on the view's
7125 // `None` swallows the diagnostic silently. This pin makes the
7126 // expectation a typed invariant.
7127 let c = caixa_with_restart_window(Some("1.5s"));
7128 let view = c.supervisor_view().expect("Supervisor kind has a view");
7129 assert_eq!(
7130 view.restart_window, None,
7131 "view-construction path soft-swallows the parse error to None"
7132 );
7133 // And the manifest-level validator does NOT soft-swallow:
7134 assert!(
7135 matches!(
7136 c.validate_restart_window().unwrap_err(),
7137 ManifestError::RestartWindowMalformed { ref restart_window, .. }
7138 if restart_window == "1.5s"
7139 ),
7140 "validator must surface the offending value",
7141 );
7142 }
7143
7144 // ── validate_code_paths — per-entry shape on :bibliotecas / :exe / :servicos ──
7145
7146 fn caixa_with_code_paths(bibliotecas: Vec<&str>, exe: Vec<&str>, servicos: Vec<&str>) -> Caixa {
7147 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
7148 c.bibliotecas = bibliotecas.into_iter().map(String::from).collect();
7149 c.exe = exe.into_iter().map(String::from).collect();
7150 c.servicos = servicos.into_iter().map(String::from).collect();
7151 c
7152 }
7153
7154 #[test]
7155 fn validate_code_paths_accepts_canonical_template() {
7156 // The bare `Caixa::template` shape is the gate's identity element
7157 // on the canonical authoring shape — `:bibliotecas
7158 // ("lib/demo.lisp")` + empty `:exe` + empty `:servicos`. Pins
7159 // that the gate is non-disruptive against every existing caixa.
7160 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
7161 c.validate_code_paths().unwrap();
7162 }
7163
7164 #[test]
7165 fn validate_code_paths_accepts_explicit_relative_paths_on_every_slot() {
7166 // Positive control sweep: a canonical-shaped path on every slot
7167 // passes. Mirrors the peer
7168 // `behavior::validate_every_slot_relative_is_ok` pin.
7169 let c = caixa_with_code_paths(
7170 vec!["lib/demo.lisp", "lib/helpers.lisp"],
7171 vec!["exe/demo", "exe/tool"],
7172 vec!["servicos/demo.computeunit.yaml"],
7173 );
7174 c.validate_code_paths().unwrap();
7175 }
7176
7177 #[test]
7178 fn validate_code_paths_accepts_all_empty_lists() {
7179 // The empty-list identity element: every Caixa with no declared
7180 // code paths trivially passes (Supervisor / Aplicacao kinds rely
7181 // on this — the OwnCode gate already rejected them before the
7182 // path-shape gate runs in the layout, but the validator itself
7183 // must accept the empty shape).
7184 let c = caixa_with_code_paths(vec![], vec![], vec![]);
7185 c.validate_code_paths().unwrap();
7186 }
7187
7188 #[test]
7189 fn validate_code_paths_rejects_empty_bibliotecas_entry() {
7190 let c = caixa_with_code_paths(vec![""], vec![], vec![]);
7191 let err = c.validate_code_paths().unwrap_err();
7192 assert!(
7193 matches!(
7194 err,
7195 ManifestError::CodePathEmpty {
7196 slot: ":bibliotecas"
7197 }
7198 ),
7199 "got {err:?}",
7200 );
7201 }
7202
7203 #[test]
7204 fn validate_code_paths_rejects_empty_exe_entry() {
7205 let c = caixa_with_code_paths(vec![], vec![""], vec![]);
7206 let err = c.validate_code_paths().unwrap_err();
7207 assert!(
7208 matches!(err, ManifestError::CodePathEmpty { slot: ":exe" }),
7209 "got {err:?}",
7210 );
7211 }
7212
7213 #[test]
7214 fn validate_code_paths_rejects_empty_servicos_entry() {
7215 let c = caixa_with_code_paths(vec![], vec![], vec![""]);
7216 let err = c.validate_code_paths().unwrap_err();
7217 assert!(
7218 matches!(err, ManifestError::CodePathEmpty { slot: ":servicos" }),
7219 "got {err:?}",
7220 );
7221 }
7222
7223 #[test]
7224 fn validate_code_paths_rejects_absolute_bibliotecas_entry() {
7225 // `:bibliotecas` has no `starts_with(<dir>)` fence downstream,
7226 // so an absolute path that resolves on disk silently passes the
7227 // layout's existence check — the canonical sandbox-escape on
7228 // the biblioteca axis.
7229 let c = caixa_with_code_paths(vec!["/etc/passwd"], vec![], vec![]);
7230 let err = c.validate_code_paths().unwrap_err();
7231 let ManifestError::CodePathAbsolute { slot, path } = err else {
7232 panic!("expected CodePathAbsolute, got {err:?}");
7233 };
7234 assert_eq!(slot, ":bibliotecas");
7235 assert_eq!(path, PathBuf::from("/etc/passwd"));
7236 }
7237
7238 #[test]
7239 fn validate_code_paths_rejects_absolute_exe_entry() {
7240 let c = caixa_with_code_paths(vec![], vec!["/usr/bin/env"], vec![]);
7241 let err = c.validate_code_paths().unwrap_err();
7242 let ManifestError::CodePathAbsolute { slot, path } = err else {
7243 panic!("expected CodePathAbsolute, got {err:?}");
7244 };
7245 assert_eq!(slot, ":exe");
7246 assert_eq!(path, PathBuf::from("/usr/bin/env"));
7247 }
7248
7249 #[test]
7250 fn validate_code_paths_rejects_absolute_servicos_entry() {
7251 let c = caixa_with_code_paths(vec![], vec![], vec!["/var/servicos/x.yaml"]);
7252 let err = c.validate_code_paths().unwrap_err();
7253 let ManifestError::CodePathAbsolute { slot, path } = err else {
7254 panic!("expected CodePathAbsolute, got {err:?}");
7255 };
7256 assert_eq!(slot, ":servicos");
7257 assert_eq!(path, PathBuf::from("/var/servicos/x.yaml"));
7258 }
7259
7260 #[test]
7261 fn validate_code_paths_rejects_parent_escape_bibliotecas_leading() {
7262 // Canonical "I want a lib from a sibling caixa" footgun on the
7263 // biblioteca axis. `:bibliotecas` has no `starts_with` fence
7264 // downstream, so a leading `..` traverses to the parent of the
7265 // caixa root with no diagnostic at layout time if the resolved
7266 // target exists.
7267 let c = caixa_with_code_paths(vec!["../sibling/x.lisp"], vec![], vec![]);
7268 let err = c.validate_code_paths().unwrap_err();
7269 let ManifestError::CodePathParentEscape { slot, path } = err else {
7270 panic!("expected CodePathParentEscape, got {err:?}");
7271 };
7272 assert_eq!(slot, ":bibliotecas");
7273 assert_eq!(path, PathBuf::from("../sibling/x.lisp"));
7274 }
7275
7276 #[test]
7277 fn validate_code_paths_rejects_parent_escape_exe_mid_path() {
7278 // Mid-path `..` defeats the layout's component-aware
7279 // `starts_with(exe_dir)` fence — `root.join("exe/../../escape")`
7280 // `starts_with(<root>/exe)` is true, but the canonical resolution
7281 // lives outside the caixa root. Caught regardless of where the
7282 // `..` sits — mirrors the peer
7283 // `behavior::validate_rejects_parent_escape_mid_path` pin.
7284 let c = caixa_with_code_paths(vec![], vec!["exe/../../escape"], vec![]);
7285 let err = c.validate_code_paths().unwrap_err();
7286 let ManifestError::CodePathParentEscape { slot, path } = err else {
7287 panic!("expected CodePathParentEscape, got {err:?}");
7288 };
7289 assert_eq!(slot, ":exe");
7290 assert_eq!(path, PathBuf::from("exe/../../escape"));
7291 }
7292
7293 #[test]
7294 fn validate_code_paths_rejects_parent_escape_servicos_trailing() {
7295 let c = caixa_with_code_paths(vec![], vec![], vec!["servicos/foo/../../escape.yaml"]);
7296 let err = c.validate_code_paths().unwrap_err();
7297 let ManifestError::CodePathParentEscape { slot, path } = err else {
7298 panic!("expected CodePathParentEscape, got {err:?}");
7299 };
7300 assert_eq!(slot, ":servicos");
7301 assert_eq!(path, PathBuf::from("servicos/foo/../../escape.yaml"));
7302 }
7303
7304 #[test]
7305 fn validate_code_paths_cross_slot_precedence_bibliotecas_before_exe_before_servicos() {
7306 // Cross-slot precedence pin: `:bibliotecas` → `:exe` →
7307 // `:servicos`. A manifest with malformed entries on all three
7308 // surfaces surfaces the `:bibliotecas` defect first, mirroring
7309 // the canonical declaration order
7310 // `Caixa::declared_foreign_code_slots` already establishes for
7311 // the foreign-code-slot diagnostic.
7312 let c = caixa_with_code_paths(vec![""], vec![""], vec![""]);
7313 let err = c.validate_code_paths().unwrap_err();
7314 assert!(
7315 matches!(
7316 err,
7317 ManifestError::CodePathEmpty {
7318 slot: ":bibliotecas"
7319 }
7320 ),
7321 "got {err:?}",
7322 );
7323 }
7324
7325 #[test]
7326 fn validate_code_paths_within_slot_precedence_empty_before_absolute_before_parent_escape() {
7327 // Within-slot precedence pin: empty → absolute → parent-escape,
7328 // matching the [`PathShapeViolation`] arm-ordering every peer
7329 // `is_sandboxed_relative_path` caller follows (b0c8389
7330 // BehaviorSpec, 26da2c7 UpgradeInstruction::StateChange). A
7331 // `:bibliotecas` list whose first entry is empty *and* whose
7332 // later entries are absolute/parent-escape surfaces the empty
7333 // arm first, on the lexicographically-earliest offending entry.
7334 let c = caixa_with_code_paths(vec!["", "/etc/passwd", "../escape.lisp"], vec![], vec![]);
7335 let err = c.validate_code_paths().unwrap_err();
7336 assert!(
7337 matches!(
7338 err,
7339 ManifestError::CodePathEmpty {
7340 slot: ":bibliotecas"
7341 }
7342 ),
7343 "got {err:?}",
7344 );
7345 }
7346
7347 #[test]
7348 fn validate_code_paths_first_offender_per_slot_wins() {
7349 // Within a single slot, the first declaration-order offender
7350 // surfaces — pins that the gate is left-to-right deterministic
7351 // (peer of every `*_first_collision_*` pin on duplicate gates).
7352 let c = caixa_with_code_paths(
7353 vec!["lib/ok.lisp", "/etc/escape", "../also-escape"],
7354 vec![],
7355 vec![],
7356 );
7357 let err = c.validate_code_paths().unwrap_err();
7358 let ManifestError::CodePathAbsolute { slot, path } = err else {
7359 panic!("expected CodePathAbsolute, got {err:?}");
7360 };
7361 assert_eq!(slot, ":bibliotecas");
7362 assert_eq!(path, PathBuf::from("/etc/escape"));
7363 }
7364
7365 #[test]
7366 fn validate_code_paths_diagnostic_carries_offending_slot_and_path() {
7367 // Diagnostic-shape pin (peer with
7368 // `nome_invalid_diagnostic_carries_offending_nome` /
7369 // `versao_invalid_diagnostic_carries_offending_versao`): the
7370 // error's Display surfaces both the offending `:slot` tag and
7371 // the offending path verbatim, so a `feira lint` run can render
7372 // the diagnostic without re-parsing.
7373 let c = caixa_with_code_paths(vec!["/etc/passwd"], vec![], vec![]);
7374 let rendered = c.validate_code_paths().unwrap_err().to_string();
7375 assert!(
7376 rendered.contains(":bibliotecas"),
7377 "diagnostic must name the offending slot: {rendered}",
7378 );
7379 assert!(
7380 rendered.contains("/etc/passwd"),
7381 "diagnostic must quote the offending path: {rendered}",
7382 );
7383 }
7384
7385 #[test]
7386 fn validate_code_paths_rejects_duplicate_bibliotecas_entry() {
7387 // Canonical copy-paste-the-wrong-file footgun on the biblioteca
7388 // axis. Without the gate `feira build` re-parses the same lib
7389 // twice, wasting work and silently masking the author's intent
7390 // to declare a *second* biblioteca.
7391 let c = caixa_with_code_paths(vec!["lib/demo.lisp", "lib/demo.lisp"], vec![], vec![]);
7392 let err = c.validate_code_paths().unwrap_err();
7393 let ManifestError::CodePathDuplicate { slot, path } = err else {
7394 panic!("expected CodePathDuplicate, got {err:?}");
7395 };
7396 assert_eq!(slot, ":bibliotecas");
7397 assert_eq!(path, PathBuf::from("lib/demo.lisp"));
7398 }
7399
7400 #[test]
7401 fn validate_code_paths_rejects_duplicate_exe_entry() {
7402 // Same footgun on the Binario surface. The future `caixa-flake`
7403 // emitter that materializes each `:exe` entry as a flake
7404 // `packages.<name>` derivation would collide on the duplicate
7405 // package key — surfaced here at the typed-validate layer with a
7406 // self-locating diagnostic instead.
7407 let c = caixa_with_code_paths(vec![], vec!["exe/cli", "exe/cli"], vec![]);
7408 let err = c.validate_code_paths().unwrap_err();
7409 let ManifestError::CodePathDuplicate { slot, path } = err else {
7410 panic!("expected CodePathDuplicate, got {err:?}");
7411 };
7412 assert_eq!(slot, ":exe");
7413 assert_eq!(path, PathBuf::from("exe/cli"));
7414 }
7415
7416 #[test]
7417 fn validate_code_paths_rejects_duplicate_servicos_entry() {
7418 // Same footgun on the Servico surface. The peer caixa-helm /
7419 // caixa-flux renderers refuse `:servicos.len() != 1` with the
7420 // narrower `UnsupportedServicoCount` diagnostic, but that
7421 // diagnostic surfaces "too many servicos" without naming
7422 // "duplicate entry" — the typed self-locating framing only lands
7423 // at this gate.
7424 let c = caixa_with_code_paths(
7425 vec![],
7426 vec![],
7427 vec![
7428 "servicos/demo.computeunit.yaml",
7429 "servicos/demo.computeunit.yaml",
7430 ],
7431 );
7432 let err = c.validate_code_paths().unwrap_err();
7433 let ManifestError::CodePathDuplicate { slot, path } = err else {
7434 panic!("expected CodePathDuplicate, got {err:?}");
7435 };
7436 assert_eq!(slot, ":servicos");
7437 assert_eq!(path, PathBuf::from("servicos/demo.computeunit.yaml"));
7438 }
7439
7440 #[test]
7441 fn validate_code_paths_accepts_same_path_across_slots() {
7442 // Per-list scope pin: a `:bibliotecas` entry that happens to
7443 // collide with an `:exe` or `:servicos` entry as a *string* is
7444 // not a duplicate by this gate (each list gets its own HashSet),
7445 // mirroring the peer `:deps` ↔ `:deps-dev` per-list scope
7446 // (a `:nome` present in both lists is a legitimate dev-vs-runtime
7447 // shape on the dep axis). The structural `starts_with(<exe |
7448 // servicos>_dir)` fence at layout time prevents the realistic
7449 // cross-slot collision case from existing on disk, but the gate's
7450 // per-list scope is correct independent of that downstream fence.
7451 let c = caixa_with_code_paths(
7452 vec!["lib/x.lisp"],
7453 vec!["exe/x"],
7454 vec!["servicos/x.computeunit.yaml"],
7455 );
7456 c.validate_code_paths().unwrap();
7457 }
7458
7459 #[test]
7460 fn validate_code_paths_duplicate_fires_after_structural_checks_on_same_slot() {
7461 // Within-slot ordering pin: structural defects (empty / absolute
7462 // / parent-escape) fire before the duplicate gate on the same
7463 // slot. A `:bibliotecas ("" "lib/x.lisp" "lib/x.lisp")` shape
7464 // surfaces the narrower `CodePathEmpty` for the empty entry
7465 // first, not the duplicate on the later pair — same arm-ordering
7466 // every peer per-list duplicate gate uses (`:etiquetas` 360a499,
7467 // `:autores` 86c769b, `:deps` 359fba5).
7468 let c = caixa_with_code_paths(vec!["", "lib/x.lisp", "lib/x.lisp"], vec![], vec![]);
7469 let err = c.validate_code_paths().unwrap_err();
7470 assert!(
7471 matches!(
7472 err,
7473 ManifestError::CodePathEmpty {
7474 slot: ":bibliotecas"
7475 }
7476 ),
7477 "got {err:?}",
7478 );
7479 }
7480
7481 #[test]
7482 fn validate_code_paths_duplicate_in_bibliotecas_fires_before_duplicate_in_exe() {
7483 // Cross-slot ordering pin on the duplicate arm: `:bibliotecas`
7484 // duplicates surface before `:exe` duplicates, matching the
7485 // canonical `:bibliotecas` → `:exe` → `:servicos` declaration
7486 // order every peer per-slot diagnostic on this surface follows.
7487 let c = caixa_with_code_paths(
7488 vec!["lib/x.lisp", "lib/x.lisp"],
7489 vec!["exe/y", "exe/y"],
7490 vec![],
7491 );
7492 let err = c.validate_code_paths().unwrap_err();
7493 let ManifestError::CodePathDuplicate { slot, path } = err else {
7494 panic!("expected CodePathDuplicate, got {err:?}");
7495 };
7496 assert_eq!(slot, ":bibliotecas");
7497 assert_eq!(path, PathBuf::from("lib/x.lisp"));
7498 }
7499
7500 #[test]
7501 fn validate_code_paths_duplicate_diagnostic_carries_offending_slot_and_path() {
7502 // Diagnostic-shape pin (peer with
7503 // `validate_code_paths_diagnostic_carries_offending_slot_and_path`
7504 // on the structural arm): the duplicate-arm Display surfaces both
7505 // the offending `:slot` tag and the offending path verbatim, so a
7506 // `feira lint` run can render the diagnostic without re-parsing.
7507 let c = caixa_with_code_paths(
7508 vec![],
7509 vec![],
7510 vec![
7511 "servicos/demo.computeunit.yaml",
7512 "servicos/demo.computeunit.yaml",
7513 ],
7514 );
7515 let rendered = c.validate_code_paths().unwrap_err().to_string();
7516 assert!(
7517 rendered.contains(":servicos"),
7518 "diagnostic must name the offending slot: {rendered}",
7519 );
7520 assert!(
7521 rendered.contains("servicos/demo.computeunit.yaml"),
7522 "diagnostic must quote the offending path: {rendered}",
7523 );
7524 }
7525
7526 // ── validate_code_paths — `.lisp` extension gate on :bibliotecas ──
7527 //
7528 // The lifted [`crate::render::is_lisp_extension`] predicate (33cc830)
7529 // now gates `:bibliotecas` entries on the tatara-lisp-source file-type
7530 // contract. The `feira build` loop (`caixa-feira/src/cmd/build.rs:33`)
7531 // reads every declared `:bibliotecas` entry through `tatara_lisp::read`
7532 // at parse time — the same downstream consumer the peer `:behavior
7533 // :on-*` (c97815a, [`crate::BehaviorError::NonLispExtension`]) and
7534 // `:upgrade-from :state-change :script` (33cc830,
7535 // [`crate::UpgradeError::NonLispExtensionScript`]) axes route through.
7536 // `:exe` and `:servicos` are deliberately excluded — `:exe` is the
7537 // nix-built executable surface (`"exe/<name>"` shape per the canonical
7538 // [`crate::LayoutError::ExeOutsideDir`] error message and every
7539 // in-tree `caixa_with_code_paths` positive control), and `:servicos`
7540 // is the `.computeunit.yaml` ComputeUnit-CR axis.
7541
7542 #[test]
7543 fn validate_code_paths_rejects_no_extension_bibliotecas_entry() {
7544 // Canonical "I dragged the wrong file from the workspace tree"
7545 // footgun on the biblioteca axis. Without the gate `feira build`
7546 // hands the extensionless path to `tatara_lisp::read` and fails
7547 // with a parser-shaped diagnostic far from the source caixa.lisp,
7548 // with no field naming the offending `:bibliotecas` entry.
7549 for relpath in ["lib/demo", "demo", "lib/handlers/inner"] {
7550 let c = caixa_with_code_paths(vec![relpath], vec![], vec![]);
7551 let err = c.validate_code_paths().unwrap_err();
7552 let ManifestError::CodePathNonLispExtension { slot, path } = err else {
7553 panic!("expected CodePathNonLispExtension for {relpath:?}, got {err:?}");
7554 };
7555 assert_eq!(slot, ":bibliotecas");
7556 assert_eq!(path, PathBuf::from(relpath));
7557 }
7558 }
7559
7560 #[test]
7561 fn validate_code_paths_rejects_wrong_extension_bibliotecas_entry() {
7562 // Wrong-extension sweep across common authoring footguns. Same
7563 // sweep posture as the peer
7564 // `behavior::validate_rejects_wrong_extension` (c97815a) and
7565 // `upgrade::tests::state_change_rejects_wrong_extension_script`
7566 // (33cc830) cases.
7567 for relpath in [
7568 "lib/demo.rs",
7569 "lib/demo.txt",
7570 "lib/demo.md",
7571 "lib/demo.json",
7572 "lib/demo.yaml",
7573 "lib/demo.toml",
7574 "lib/demo.lisp.bak",
7575 "lib/demo.lispx",
7576 "lib/demo.lis",
7577 ] {
7578 let c = caixa_with_code_paths(vec![relpath], vec![], vec![]);
7579 let err = c.validate_code_paths().unwrap_err();
7580 let ManifestError::CodePathNonLispExtension { slot, path } = err else {
7581 panic!("expected CodePathNonLispExtension for {relpath:?}, got {err:?}");
7582 };
7583 assert_eq!(slot, ":bibliotecas");
7584 assert_eq!(path, PathBuf::from(relpath));
7585 }
7586 }
7587
7588 #[test]
7589 fn validate_code_paths_rejects_case_folded_extension_bibliotecas_entry() {
7590 // Case-sensitivity sweep — pins the strict lowercase `.lisp`
7591 // contract. An uppercase `.LISP` shape that the layout's existence
7592 // check would (case-insensitively, on case-insensitive volumes)
7593 // match the on-disk file still mismatches the canonical form the
7594 // codec emits, breaking the THEORY.md §V.2.7 render-determinism
7595 // contract. Mirrors the peer
7596 // `behavior::validate_rejects_case_folded_extension` (c97815a) and
7597 // `upgrade::tests::state_change_rejects_case_folded_extension_script`
7598 // (33cc830) sweeps.
7599 for relpath in [
7600 "lib/demo.LISP",
7601 "lib/demo.Lisp",
7602 "lib/demo.LiSp",
7603 "lib/demo.lISP",
7604 ] {
7605 let c = caixa_with_code_paths(vec![relpath], vec![], vec![]);
7606 let err = c.validate_code_paths().unwrap_err();
7607 let ManifestError::CodePathNonLispExtension { slot, path } = err else {
7608 panic!("expected CodePathNonLispExtension for {relpath:?}, got {err:?}");
7609 };
7610 assert_eq!(slot, ":bibliotecas");
7611 assert_eq!(path, PathBuf::from(relpath));
7612 }
7613 }
7614
7615 #[test]
7616 fn validate_code_paths_accepts_canonical_lisp_shapes() {
7617 // Positive-control sweep through every canonical authoring shape
7618 // every in-tree fixture and the `Caixa::template` scaffold use.
7619 // Mirrors the peer `behavior::validate_accepts_canonical_lisp_paths`
7620 // (c97815a) and the lifted predicate's own
7621 // `is_lisp_extension_accepts_canonical_shapes` sweep in render.rs
7622 // (33cc830).
7623 for relpath in [
7624 "lib/demo.lisp",
7625 "lib/handlers.lisp",
7626 "lib/migrations/v01-to-v02.lisp",
7627 "demo.lisp",
7628 "a.lisp",
7629 "./lib/demo.lisp",
7630 "lib/./handlers.lisp",
7631 "lib/migrations/v.0.1.lisp",
7632 ] {
7633 let c = caixa_with_code_paths(vec![relpath], vec![], vec![]);
7634 c.validate_code_paths()
7635 .unwrap_or_else(|e| panic!("canonical shape {relpath:?} must pass, got {e:?}"));
7636 }
7637 }
7638
7639 #[test]
7640 fn validate_code_paths_non_lisp_extension_does_not_fire_on_exe_or_servicos() {
7641 // The file-type gate is per-slot — only `:bibliotecas` carries the
7642 // tatara-lisp-source contract. An extensionless `:exe` entry
7643 // (`exe/demo`) and a `.computeunit.yaml` `:servicos` entry are the
7644 // canonical shapes every in-tree fixture uses, and must continue
7645 // to pass validate. Pins that a future tightening that broadens
7646 // the `.lisp` gate to either axis surfaces as a test failure
7647 // rather than as a silent breaking change to existing valid
7648 // manifests.
7649 let c = caixa_with_code_paths(
7650 vec![],
7651 vec!["exe/demo", "exe/tool"],
7652 vec!["servicos/demo.computeunit.yaml"],
7653 );
7654 c.validate_code_paths().unwrap();
7655 }
7656
7657 #[test]
7658 fn validate_code_paths_sandbox_shape_arms_precede_non_lisp_extension() {
7659 // Cross-arm precedence pin: a `:bibliotecas` entry that is *both*
7660 // sandbox-escaping and non-`.lisp` surfaces the more fundamental
7661 // sandbox-shape diagnostic first (the `.lisp` remediation would
7662 // be misleading when the offending path can never resolve under
7663 // the caixa root anyway). Mirrors the peer
7664 // `EmptyPath` → `AbsolutePath` → `ParentEscape` → `NonLispExtension`
7665 // ordering on `:behavior :on-*` (c97815a) and `EmptyScript` →
7666 // `AbsoluteScript` → `ParentEscapeScript` → `NonLispExtensionScript`
7667 // on `:upgrade-from :state-change :script` (33cc830).
7668 //
7669 // Empty wins (the strictly-smaller-scope structural arm).
7670 let c = caixa_with_code_paths(vec![""], vec![], vec![]);
7671 assert!(
7672 matches!(
7673 c.validate_code_paths().unwrap_err(),
7674 ManifestError::CodePathEmpty {
7675 slot: ":bibliotecas"
7676 }
7677 ),
7678 "empty must win over non-lisp-extension",
7679 );
7680 // Absolute wins (the path can't resolve under the caixa root).
7681 let c = caixa_with_code_paths(vec!["/etc/passwd"], vec![], vec![]);
7682 let err = c.validate_code_paths().unwrap_err();
7683 let ManifestError::CodePathAbsolute { slot, .. } = err else {
7684 panic!("absolute must win over non-lisp-extension, got {err:?}");
7685 };
7686 assert_eq!(slot, ":bibliotecas");
7687 // ParentEscape wins (the path escapes the caixa root).
7688 let c = caixa_with_code_paths(vec!["../sibling/x.txt"], vec![], vec![]);
7689 let err = c.validate_code_paths().unwrap_err();
7690 let ManifestError::CodePathParentEscape { slot, .. } = err else {
7691 panic!("parent-escape must win over non-lisp-extension, got {err:?}");
7692 };
7693 assert_eq!(slot, ":bibliotecas");
7694 }
7695
7696 #[test]
7697 fn validate_code_paths_non_lisp_extension_precedes_duplicate() {
7698 // Within-slot precedence pin: the per-entry file-type shape gate
7699 // fires before the cross-entry duplicate gate, so the narrower
7700 // structural defect dominates the uniqueness diagnostic. A
7701 // `("lib/x.txt" "lib/x.txt")` shape surfaces
7702 // `CodePathNonLispExtension` on the first entry rather than
7703 // `CodePathDuplicate` on the pair — same posture every per-entry
7704 // shape-gate-precedes-duplicate cascade follows on this surface
7705 // (the empty / absolute / parent-escape arms already precede the
7706 // duplicate arm; the lifted file-type arm joins that set).
7707 let c = caixa_with_code_paths(vec!["lib/x.txt", "lib/x.txt"], vec![], vec![]);
7708 let err = c.validate_code_paths().unwrap_err();
7709 let ManifestError::CodePathNonLispExtension { slot, path } = err else {
7710 panic!("expected CodePathNonLispExtension, got {err:?}");
7711 };
7712 assert_eq!(slot, ":bibliotecas");
7713 assert_eq!(path, PathBuf::from("lib/x.txt"));
7714 }
7715
7716 #[test]
7717 fn validate_code_paths_non_lisp_extension_diagnostic_carries_offending_slot_and_path() {
7718 // Diagnostic-shape pin (peer with
7719 // `validate_code_paths_diagnostic_carries_offending_slot_and_path`
7720 // on the sandbox-shape arms and
7721 // `validate_code_paths_duplicate_diagnostic_carries_offending_slot_and_path`
7722 // on the duplicate arm): the file-type-arm Display surfaces both
7723 // the offending `:slot` tag, the offending path verbatim, and the
7724 // expected `.lisp` extension named in the remediation text, so a
7725 // `feira lint` run can render the diagnostic without re-parsing.
7726 let c = caixa_with_code_paths(vec!["lib/demo.rs"], vec![], vec![]);
7727 let rendered = c.validate_code_paths().unwrap_err().to_string();
7728 assert!(
7729 rendered.contains(":bibliotecas"),
7730 "diagnostic must name the offending slot: {rendered}",
7731 );
7732 assert!(
7733 rendered.contains("lib/demo.rs"),
7734 "diagnostic must quote the offending path: {rendered}",
7735 );
7736 assert!(
7737 rendered.contains(".lisp"),
7738 "diagnostic must name the expected extension: {rendered}",
7739 );
7740 }
7741
7742 // ── validate_code_paths — `.computeunit.yaml` compound-suffix gate on :servicos ──
7743 //
7744 // The lifted [`crate::render::is_computeunit_yaml_extension`] predicate
7745 // now gates `:servicos` entries on the ComputeUnit-CR YAML file-type
7746 // contract. The peer caixa-helm / caixa-flux renderers consume each
7747 // `:servicos` entry through `serde_yaml::from_str` as a typed
7748 // `ComputeUnit` CR — same downstream-consumer-shape lift as the peer
7749 // `:bibliotecas` `.lisp` gate (64772a9), here on the compound-suffix
7750 // axis `Path::extension` can't express on its own.
7751
7752 #[test]
7753 fn validate_code_paths_rejects_no_extension_servicos_entry() {
7754 // Canonical "I dragged the wrong file from the workspace tree"
7755 // footgun on the Servico axis. Without the gate the peer
7756 // caixa-helm / caixa-flux renderers hand the extensionless path
7757 // to `serde_yaml::from_str` and fail with a parser-shaped
7758 // diagnostic far from the source caixa.lisp, with no field
7759 // naming the offending `:servicos` entry.
7760 for relpath in ["servicos/demo", "demo", "servicos/sub/nested"] {
7761 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
7762 let err = c.validate_code_paths().unwrap_err();
7763 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
7764 panic!(
7765 "expected CodePathNonComputeUnitYamlExtension for {relpath:?}, \
7766 got {err:?}"
7767 );
7768 };
7769 assert_eq!(slot, ":servicos");
7770 assert_eq!(path, PathBuf::from(relpath));
7771 }
7772 }
7773
7774 #[test]
7775 fn validate_code_paths_rejects_wrong_extension_servicos_entry() {
7776 // Wrong-extension sweep across common authoring footguns on the
7777 // Servico axis. Bare `.yaml` is the canonical "I forgot the
7778 // `.computeunit` segment" typo; the off-by-one-segment shapes
7779 // (`.computeunit-yaml` / `.computeunit_yaml`) silently pass the
7780 // bare `Path::extension` view but mismatch the typed compound
7781 // suffix the renderers' `serde_yaml::from_str` consumer demands.
7782 // Same sweep-posture as the peer
7783 // `validate_code_paths_rejects_wrong_extension_bibliotecas_entry`
7784 // (64772a9) on the sibling tatara-lisp-source axis.
7785 for relpath in [
7786 "servicos/demo.yaml",
7787 "servicos/demo.yml",
7788 "servicos/demo.json",
7789 "servicos/demo.toml",
7790 "servicos/demo.txt",
7791 "servicos/demo.computeunit.yaml.bak",
7792 "servicos/demo.computeunit.yam",
7793 "servicos/demo.computeunit",
7794 "servicos/demo-computeunit.yaml",
7795 "servicos/demo_computeunit.yaml",
7796 ] {
7797 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
7798 let err = c.validate_code_paths().unwrap_err();
7799 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
7800 panic!(
7801 "expected CodePathNonComputeUnitYamlExtension for {relpath:?}, \
7802 got {err:?}"
7803 );
7804 };
7805 assert_eq!(slot, ":servicos");
7806 assert_eq!(path, PathBuf::from(relpath));
7807 }
7808 }
7809
7810 #[test]
7811 fn validate_code_paths_rejects_case_folded_extension_servicos_entry() {
7812 // Case-sensitivity sweep — pins the strict lowercase
7813 // `.computeunit.yaml` contract. A case-folded shape that the
7814 // layout's existence check would (case-insensitively, on
7815 // case-insensitive volumes) match the on-disk file still
7816 // mismatches the canonical form the codec emits, breaking the
7817 // THEORY.md §V.2.7 render-determinism contract. Mirrors the peer
7818 // `validate_code_paths_rejects_case_folded_extension_bibliotecas_entry`
7819 // (64772a9) sweep on the sibling tatara-lisp-source axis.
7820 for relpath in [
7821 "servicos/demo.ComputeUnit.yaml",
7822 "servicos/demo.COMPUTEUNIT.yaml",
7823 "servicos/demo.computeunit.YAML",
7824 "servicos/demo.computeunit.Yaml",
7825 "servicos/demo.COMPUTEUNIT.YAML",
7826 ] {
7827 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
7828 let err = c.validate_code_paths().unwrap_err();
7829 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
7830 panic!(
7831 "expected CodePathNonComputeUnitYamlExtension for {relpath:?}, \
7832 got {err:?}"
7833 );
7834 };
7835 assert_eq!(slot, ":servicos");
7836 assert_eq!(path, PathBuf::from(relpath));
7837 }
7838 }
7839
7840 #[test]
7841 fn validate_code_paths_rejects_empty_stem_servicos_entry() {
7842 // Degenerate hidden-file shape: a file name exactly equal to the
7843 // suffix (`.computeunit.yaml` — no stem preceding the suffix) is
7844 // the structural "Servico declared with no identity" footgun.
7845 // The substrate identifies each ComputeUnit by the file-stem
7846 // segment that precedes `.computeunit.yaml` (the rendered
7847 // `lareira-<stem>` Helm chart, the per-Servico `metadata.name`,
7848 // the M3 `:contratos` membership lookup), so an empty stem
7849 // leaves the Servico unidentifiable. Pinned at the typed-axis
7850 // level so a future regression that drops the `name.len() >
7851 // SUFFIX.len()` bound at the predicate surfaces here, not
7852 // piecemeal as a `lareira-` chart-name collision at render time.
7853 for relpath in ["servicos/.computeunit.yaml"] {
7854 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
7855 let err = c.validate_code_paths().unwrap_err();
7856 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
7857 panic!(
7858 "expected CodePathNonComputeUnitYamlExtension for {relpath:?}, \
7859 got {err:?}"
7860 );
7861 };
7862 assert_eq!(slot, ":servicos");
7863 assert_eq!(path, PathBuf::from(relpath));
7864 }
7865 }
7866
7867 #[test]
7868 fn validate_code_paths_accepts_canonical_computeunit_yaml_shapes() {
7869 // Positive-control sweep through every canonical authoring shape
7870 // every in-tree fixture and the `Caixa::template` scaffold use.
7871 // Mirrors the peer
7872 // `validate_code_paths_accepts_canonical_lisp_shapes` (64772a9)
7873 // and the lifted predicate's own
7874 // `computeunit_yaml_extension_accepts_canonical_shapes` sweep in
7875 // render.rs.
7876 for relpath in [
7877 "servicos/demo.computeunit.yaml",
7878 "servicos/hello-rio.computeunit.yaml",
7879 "servicos/my-service.computeunit.yaml",
7880 "servicos/a.computeunit.yaml",
7881 "./servicos/demo.computeunit.yaml",
7882 "servicos/./demo.computeunit.yaml",
7883 "servicos/sub/nested.computeunit.yaml",
7884 "servicos/v0.1.computeunit.yaml",
7885 ] {
7886 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
7887 c.validate_code_paths()
7888 .unwrap_or_else(|e| panic!("canonical shape {relpath:?} must pass, got {e:?}"));
7889 }
7890 }
7891
7892 #[test]
7893 fn validate_code_paths_non_computeunit_yaml_extension_does_not_fire_on_bibliotecas_or_exe() {
7894 // The file-type gate is per-slot — only `:servicos` carries the
7895 // ComputeUnit-CR YAML contract. A canonical `.lisp` `:bibliotecas`
7896 // entry and an extensionless `:exe` entry are the canonical
7897 // shapes every in-tree fixture uses, and must continue to pass
7898 // validate. Peer of
7899 // `validate_code_paths_non_lisp_extension_does_not_fire_on_exe_or_servicos`
7900 // (64772a9) — together pin that the typed
7901 // [`CodePathFileType`] dispatch is exhaustively per-slot, with no
7902 // cross-axis leakage in either direction.
7903 let c = caixa_with_code_paths(
7904 vec!["lib/demo.lisp"],
7905 vec!["exe/demo", "exe/tool"],
7906 vec!["servicos/demo.computeunit.yaml"],
7907 );
7908 c.validate_code_paths().unwrap();
7909 }
7910
7911 #[test]
7912 fn validate_code_paths_sandbox_shape_arms_precede_non_computeunit_yaml_extension() {
7913 // Cross-arm precedence pin: a `:servicos` entry that is *both*
7914 // sandbox-escaping and wrong-extension surfaces the more
7915 // fundamental sandbox-shape diagnostic first (the
7916 // `.computeunit.yaml` remediation would be misleading when the
7917 // offending path can never resolve under the caixa root
7918 // anyway). Mirrors the peer
7919 // `validate_code_paths_sandbox_shape_arms_precede_non_lisp_extension`
7920 // (64772a9) ordering on the sibling `:bibliotecas` axis and the
7921 // peer `EmptyPath` → `AbsolutePath` → `ParentEscape` →
7922 // `NonComputeUnitYamlExtension` arm-ordering the dispatch
7923 // table establishes.
7924 //
7925 // Empty wins (the strictly-smaller-scope structural arm).
7926 let c = caixa_with_code_paths(vec![], vec![], vec![""]);
7927 assert!(
7928 matches!(
7929 c.validate_code_paths().unwrap_err(),
7930 ManifestError::CodePathEmpty { slot: ":servicos" }
7931 ),
7932 "empty must win over non-computeunit-yaml-extension",
7933 );
7934 // Absolute wins (the path can't resolve under the caixa root).
7935 let c = caixa_with_code_paths(vec![], vec![], vec!["/etc/foo.yaml"]);
7936 let err = c.validate_code_paths().unwrap_err();
7937 let ManifestError::CodePathAbsolute { slot, .. } = err else {
7938 panic!("absolute must win over non-computeunit-yaml-extension, got {err:?}");
7939 };
7940 assert_eq!(slot, ":servicos");
7941 // ParentEscape wins (the path escapes the caixa root).
7942 let c = caixa_with_code_paths(vec![], vec![], vec!["../sibling/x.yaml"]);
7943 let err = c.validate_code_paths().unwrap_err();
7944 let ManifestError::CodePathParentEscape { slot, .. } = err else {
7945 panic!("parent-escape must win over non-computeunit-yaml-extension, got {err:?}");
7946 };
7947 assert_eq!(slot, ":servicos");
7948 }
7949
7950 #[test]
7951 fn validate_code_paths_non_computeunit_yaml_extension_precedes_duplicate() {
7952 // Within-slot precedence pin: the per-entry file-type shape gate
7953 // fires before the cross-entry duplicate gate, so the narrower
7954 // structural defect dominates the uniqueness diagnostic. A
7955 // `("servicos/x.yaml" "servicos/x.yaml")` shape surfaces
7956 // `CodePathNonComputeUnitYamlExtension` on the first entry
7957 // rather than `CodePathDuplicate` on the pair — same posture
7958 // every per-entry shape-gate-precedes-duplicate cascade follows
7959 // on this surface, peer of the 64772a9 `:bibliotecas`
7960 // `("lib/x.txt" "lib/x.txt")` ordering.
7961 let c = caixa_with_code_paths(vec![], vec![], vec!["servicos/x.yaml", "servicos/x.yaml"]);
7962 let err = c.validate_code_paths().unwrap_err();
7963 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
7964 panic!("expected CodePathNonComputeUnitYamlExtension, got {err:?}");
7965 };
7966 assert_eq!(slot, ":servicos");
7967 assert_eq!(path, PathBuf::from("servicos/x.yaml"));
7968 }
7969
7970 #[test]
7971 fn validate_code_paths_non_computeunit_yaml_extension_diagnostic_carries_offending_slot_and_path()
7972 {
7973 // Diagnostic-shape pin (peer with
7974 // `validate_code_paths_non_lisp_extension_diagnostic_carries_offending_slot_and_path`
7975 // on the sibling tatara-lisp-source axis): the file-type-arm
7976 // Display surfaces both the offending `:slot` tag, the
7977 // offending path verbatim, and the expected
7978 // `.computeunit.yaml` compound suffix named in the remediation
7979 // text, so a `feira lint` run can render the diagnostic without
7980 // re-parsing.
7981 let c = caixa_with_code_paths(vec![], vec![], vec!["servicos/demo.yaml"]);
7982 let rendered = c.validate_code_paths().unwrap_err().to_string();
7983 assert!(
7984 rendered.contains(":servicos"),
7985 "diagnostic must name the offending slot: {rendered}",
7986 );
7987 assert!(
7988 rendered.contains("servicos/demo.yaml"),
7989 "diagnostic must quote the offending path: {rendered}",
7990 );
7991 assert!(
7992 rendered.contains(".computeunit.yaml"),
7993 "diagnostic must name the expected compound suffix: {rendered}",
7994 );
7995 }
7996
7997 // ── validate_etiquetas — universal-axis registry-search-tag shape ──
7998
7999 fn caixa_with_etiquetas(etiquetas: Vec<&str>) -> Caixa {
8000 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
8001 c.etiquetas = etiquetas.into_iter().map(String::from).collect();
8002 c
8003 }
8004
8005 #[test]
8006 fn validate_etiquetas_accepts_empty_list() {
8007 // The empty-list identity: every caixa with no declared tags
8008 // trivially passes — `Caixa::template` emits `:etiquetas ()`,
8009 // so the gate is non-disruptive against every existing manifest.
8010 let c = caixa_with_etiquetas(vec![]);
8011 c.validate_etiquetas().unwrap();
8012 }
8013
8014 #[test]
8015 fn validate_etiquetas_accepts_canonical_forms() {
8016 // Positive control sweep: a canonical-shaped non-empty distinct
8017 // tag list passes, mirroring the example checkout-aplicacao
8018 // (`:etiquetas ("example" "aplicacao" "mesh" "ecommerce" "demo")`)
8019 // and the hello-rio fixture (`("hello-world" "wasm" "rust")`).
8020 let c = caixa_with_etiquetas(vec!["example", "aplicacao", "mesh", "ecommerce", "demo"]);
8021 c.validate_etiquetas().unwrap();
8022 }
8023
8024 #[test]
8025 fn validate_etiquetas_rejects_empty_entry() {
8026 // Canonical paste-from-blank-doc footgun. Without the gate the
8027 // empty entry rendered as `keywords: [""]` in `Chart.yaml`, a
8028 // no-op tag indexing nothing in the future caixa-registry.
8029 let c = caixa_with_etiquetas(vec![""]);
8030 let err = c.validate_etiquetas().unwrap_err();
8031 assert!(matches!(err, ManifestError::EtiquetaEmpty), "got {err:?}",);
8032 }
8033
8034 #[test]
8035 fn validate_etiquetas_rejects_duplicate_entry() {
8036 // Canonical copy-paste-the-wrong-tag footgun. Without the gate
8037 // the duplicate was silently dedup'd by caixa-helm's BTreeSet
8038 // collect at chart render — a "second wins / one silently
8039 // disappears" shape divergent from every peer typed-graph set
8040 // gate. The duplicate-arm names the offending tag verbatim.
8041 let c = caixa_with_etiquetas(vec!["demo", "demo"]);
8042 let err = c.validate_etiquetas().unwrap_err();
8043 let ManifestError::EtiquetaDuplicate { etiqueta } = err else {
8044 panic!("expected EtiquetaDuplicate, got {err:?}");
8045 };
8046 assert_eq!(etiqueta, "demo");
8047 }
8048
8049 #[test]
8050 fn validate_etiquetas_empty_takes_precedence_over_duplicate() {
8051 // Empty-first cascade pin: `("" "demo" "demo")` surfaces
8052 // `EtiquetaEmpty` not `EtiquetaDuplicate` — the narrower
8053 // structural "this entry has no value" defect dominates the
8054 // cross-entry uniqueness diagnostic. Mirrors the peer
8055 // empty-before-duplicate cascades on `:caracteristicas`
8056 // (`CaracteristicaEmpty` before `CaracteristicaDuplicate`,
8057 // fc3b4d5) and `:membros :caixa` (`MembroCaixaEmpty` before
8058 // `MembroDuplicate`).
8059 let c = caixa_with_etiquetas(vec!["", "demo", "demo"]);
8060 let err = c.validate_etiquetas().unwrap_err();
8061 assert!(matches!(err, ManifestError::EtiquetaEmpty), "got {err:?}",);
8062 }
8063
8064 #[test]
8065 fn validate_etiquetas_duplicate_reports_first_collision() {
8066 // First-collision pin: `("a" "b" "a" "b")` surfaces the `"a"`
8067 // duplicate (the lexicographically-earliest offending position
8068 // — the second `"a"` at index 2 collides with the first `"a"`
8069 // at index 0), not the later `"b"` collision at index 3,
8070 // peer with every other first-collision diagnostic posture on
8071 // this surface (`validate_load_singularity_reports_first_collision`,
8072 // `validate_cleanup_singularity_reports_first_collision`).
8073 let c = caixa_with_etiquetas(vec!["a", "b", "a", "b"]);
8074 let err = c.validate_etiquetas().unwrap_err();
8075 let ManifestError::EtiquetaDuplicate { etiqueta } = err else {
8076 panic!("expected EtiquetaDuplicate, got {err:?}");
8077 };
8078 assert_eq!(etiqueta, "a");
8079 }
8080
8081 #[test]
8082 fn validate_etiquetas_case_sensitive() {
8083 // Case-sensitivity pin: `("Foo" "foo")` is two distinct entries,
8084 // mirroring the peer `:membros :caixa` / `:children :caixa`
8085 // exact-string-match discipline. The shape gate this routine
8086 // landed (`is_chart_keyword_shape`, Cargo crates.io keyword
8087 // grammar) accepts mixed case — crates.io's keyword rule is
8088 // "case-insensitive" at the index layer but admits mixed case
8089 // at the entry layer (the canonical Helm chart `keywords:`
8090 // shape is lowercase by convention, but the grammar admits
8091 // uppercase). Case-sensitivity at the duplicate-set layer
8092 // remains structural — two distinct strings are two distinct
8093 // entries.
8094 let c = caixa_with_etiquetas(vec!["Foo", "foo"]);
8095 c.validate_etiquetas().unwrap();
8096 }
8097
8098 #[test]
8099 fn validate_etiquetas_diagnostic_carries_offending_tag() {
8100 // Diagnostic-shape pin (peer with
8101 // `validate_code_paths_diagnostic_carries_offending_slot_and_path`):
8102 // the error's Display surfaces the offending tag verbatim, so a
8103 // `feira lint` run can render the diagnostic without re-parsing
8104 // and the author can grep their caixa.lisp for the offending
8105 // value.
8106 let c = caixa_with_etiquetas(vec!["demo", "demo"]);
8107 let rendered = c.validate_etiquetas().unwrap_err().to_string();
8108 assert!(
8109 rendered.contains(":etiquetas"),
8110 "diagnostic must name the offending slot: {rendered}",
8111 );
8112 assert!(
8113 rendered.contains("demo"),
8114 "diagnostic must quote the offending tag: {rendered}",
8115 );
8116 }
8117
8118 #[test]
8119 fn validate_etiquetas_rejects_leading_whitespace_entry() {
8120 // Canonical paste-from-aligned-doc footgun. Without the shape
8121 // gate `" mesh"` silently passed validate and landed as a
8122 // YAML plain-style scalar with leading whitespace in the
8123 // rendered Chart.yaml `keywords:` array — every YAML 1.2
8124 // dumper trims leading whitespace from plain-style scalars,
8125 // so the authored space round-tripped inconsistently back
8126 // through `caixa.lisp`. Mirrors the peer
8127 // `validate_autores_rejects_leading_whitespace_entry`.
8128 let c = caixa_with_etiquetas(vec![" mesh"]);
8129 let err = c.validate_etiquetas().unwrap_err();
8130 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8131 panic!("expected EtiquetaInvalid, got {err:?}");
8132 };
8133 assert_eq!(etiqueta, " mesh");
8134 assert!(reason.contains("whitespace"), "got: {reason}");
8135 }
8136
8137 #[test]
8138 fn validate_etiquetas_rejects_embedded_newline_entry() {
8139 // Canonical paste-from-multiline-doc footgun — the author
8140 // pasted a multi-tag block into one `:etiquetas` entry
8141 // instead of splitting into one entry per tag. Without the
8142 // shape gate `"mesh\nhttp"` silently passed validate and
8143 // landed as a YAML-illegal multi-line scalar in the rendered
8144 // Chart.yaml `keywords:` array.
8145 let c = caixa_with_etiquetas(vec!["mesh\nhttp"]);
8146 let err = c.validate_etiquetas().unwrap_err();
8147 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8148 panic!("expected EtiquetaInvalid, got {err:?}");
8149 };
8150 assert_eq!(etiqueta, "mesh\nhttp");
8151 assert!(reason.contains("newline"), "got: {reason}");
8152 }
8153
8154 #[test]
8155 fn validate_etiquetas_rejects_embedded_comma_entry() {
8156 // Canonical CSV-list-separator-confusion footgun: the author
8157 // confused the CSV-style separator convention with the
8158 // `:etiquetas` list grammar. Without the shape gate
8159 // `"mesh,http,grpc"` silently passed validate and landed as a
8160 // single malformed search tag in the rendered Chart.yaml
8161 // `keywords:` array — Artifact Hub's keyword index would
8162 // either silently drop the tag or index it as
8163 // `mesh,http,grpc` instead of three separate tags.
8164 let c = caixa_with_etiquetas(vec!["mesh,http,grpc"]);
8165 let err = c.validate_etiquetas().unwrap_err();
8166 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8167 panic!("expected EtiquetaInvalid, got {err:?}");
8168 };
8169 assert_eq!(etiqueta, "mesh,http,grpc");
8170 assert!(reason.contains('`'), "got: {reason}");
8171 assert!(reason.contains(','), "got: {reason}");
8172 }
8173
8174 #[test]
8175 fn validate_etiquetas_rejects_embedded_slash_entry() {
8176 // Canonical path-separator-confusion footgun: the author
8177 // confused namespace-path notation with the keyword grammar.
8178 let c = caixa_with_etiquetas(vec!["caixa/servico"]);
8179 let err = c.validate_etiquetas().unwrap_err();
8180 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8181 panic!("expected EtiquetaInvalid, got {err:?}");
8182 };
8183 assert_eq!(etiqueta, "caixa/servico");
8184 assert!(reason.contains('/'), "got: {reason}");
8185 }
8186
8187 #[test]
8188 fn validate_etiquetas_rejects_leading_digit_entry() {
8189 // Canonical paste-from-numbered-list footgun: the author
8190 // copied `1. mesh` from a numbered doc and the `1` leaked
8191 // into the tag.
8192 let c = caixa_with_etiquetas(vec!["1mesh"]);
8193 let err = c.validate_etiquetas().unwrap_err();
8194 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8195 panic!("expected EtiquetaInvalid, got {err:?}");
8196 };
8197 assert_eq!(etiqueta, "1mesh");
8198 assert!(reason.contains("digit"), "got: {reason}");
8199 }
8200
8201 #[test]
8202 fn validate_etiquetas_rejects_leading_hyphen_entry() {
8203 // Canonical kebab-leak footgun.
8204 let c = caixa_with_etiquetas(vec!["-foo"]);
8205 let err = c.validate_etiquetas().unwrap_err();
8206 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8207 panic!("expected EtiquetaInvalid, got {err:?}");
8208 };
8209 assert_eq!(etiqueta, "-foo");
8210 assert!(reason.contains('-'), "got: {reason}");
8211 }
8212
8213 #[test]
8214 fn validate_etiquetas_rejects_non_ascii_entry() {
8215 // Canonical paste-from-Unicode-doc footgun. Every legitimate
8216 // search tag is strict ASCII; raw non-ASCII silently
8217 // round-trips inconsistently across NFC/NFD normalization on
8218 // APFS / case-folding filesystems and breaks the Artifact Hub
8219 // keyword search index lookup.
8220 let c = caixa_with_etiquetas(vec!["café"]);
8221 let err = c.validate_etiquetas().unwrap_err();
8222 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8223 panic!("expected EtiquetaInvalid, got {err:?}");
8224 };
8225 assert_eq!(etiqueta, "café");
8226 assert!(reason.contains("non-ASCII"), "got: {reason}");
8227 }
8228
8229 #[test]
8230 fn validate_etiquetas_rejects_period_entry() {
8231 // Canonical namespace-confusion / version-suffix footgun
8232 // (`"http.1"` / `"v1.0"`): Cargo's crates.io keyword grammar
8233 // excludes `.` from the continuation set even though the
8234 // sibling `:caracteristicas` axis (Cargo's feature-name
8235 // grammar) admits it. Tighter than the sibling axis, peer
8236 // with Cargo's own crates.io keyword shape.
8237 let c = caixa_with_etiquetas(vec!["http.1"]);
8238 let err = c.validate_etiquetas().unwrap_err();
8239 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8240 panic!("expected EtiquetaInvalid, got {err:?}");
8241 };
8242 assert_eq!(etiqueta, "http.1");
8243 assert!(reason.contains('.'), "got: {reason}");
8244 }
8245
8246 #[test]
8247 fn validate_etiquetas_empty_takes_precedence_over_shape() {
8248 // Per-entry empty-first cascade pin: an entry that is both
8249 // empty *and* shape-invalid surfaces `EtiquetaEmpty` (the
8250 // narrower "this entry has no value" structural defect
8251 // dominates the broader shape-predicate diagnostic). The
8252 // empty arm fires before the shape predicate is consulted,
8253 // mirroring the peer `validate_autores_empty_takes_precedence_over_shape`
8254 // cascade established on the sibling universal-axis Vec<String>
8255 // surface.
8256 let c = caixa_with_etiquetas(vec![""]);
8257 let err = c.validate_etiquetas().unwrap_err();
8258 assert!(matches!(err, ManifestError::EtiquetaEmpty), "got {err:?}",);
8259 }
8260
8261 #[test]
8262 fn validate_etiquetas_shape_takes_precedence_over_duplicate() {
8263 // Per-entry shape-before-cross-entry-duplicate cascade pin: an
8264 // entry that is malformed surfaces `EtiquetaInvalid` even when
8265 // a later entry would have collided on duplicate. The
8266 // per-entry shape arm fires inside the same loop iteration as
8267 // the empty arm, before the seen-set insert at end-of-iteration
8268 // — structural per-entry defects dominate the cross-entry
8269 // uniqueness diagnostic. Mirrors the peer
8270 // `validate_autores_shape_takes_precedence_over_duplicate`.
8271 let c = caixa_with_etiquetas(vec!["mesh\nhttp", "mesh\nhttp"]);
8272 let err = c.validate_etiquetas().unwrap_err();
8273 assert!(
8274 matches!(err, ManifestError::EtiquetaInvalid { .. }),
8275 "got {err:?}",
8276 );
8277 }
8278
8279 #[test]
8280 fn validate_etiquetas_invalid_diagnostic_names_offending_slot_and_value() {
8281 // Diagnostic-shape pin on the new shape arm (peer with
8282 // `validate_autores_invalid_diagnostic_names_offending_slot_and_value`):
8283 // the rendered Display surfaces both the offending slot name
8284 // and the offending value verbatim, so a `feira lint` run
8285 // points the author at the exact `:etiquetas` entry to fix.
8286 let c = caixa_with_etiquetas(vec!["mesh\nhttp"]);
8287 let rendered = c.validate_etiquetas().unwrap_err().to_string();
8288 assert!(
8289 rendered.contains(":etiquetas"),
8290 "diagnostic must name the offending slot: {rendered}",
8291 );
8292 assert!(
8293 rendered.contains("mesh\\nhttp"),
8294 "diagnostic must quote the offending value (debug-escaped): {rendered}",
8295 );
8296 }
8297
8298 #[test]
8299 fn validate_etiquetas_rejects_at_21_byte_boundary() {
8300 // The 20-byte cap pin — boundary-exceeding case rejected,
8301 // boundary-accepting case passes. Mirrors the peer
8302 // `chart_keyword_shape_rejects_at_21_byte_boundary` substrate-
8303 // side pin, surfaced at the per-axis caller so the cap
8304 // propagates through validate end-to-end. Constructed as a
8305 // single all-`a` token so only the cap arm fires.
8306 let max_ok = "a".repeat(20);
8307 let c = caixa_with_etiquetas(vec![max_ok.as_str()]);
8308 c.validate_etiquetas().unwrap();
8309 let too_long = "a".repeat(21);
8310 let c = caixa_with_etiquetas(vec![too_long.as_str()]);
8311 let err = c.validate_etiquetas().unwrap_err();
8312 let ManifestError::EtiquetaInvalid { reason, .. } = err else {
8313 panic!("expected EtiquetaInvalid, got {err:?}");
8314 };
8315 assert!(reason.contains("20"), "got: {reason}");
8316 assert!(reason.contains("21"), "got: {reason}");
8317 }
8318
8319 #[test]
8320 fn validate_etiquetas_accepts_canonical_shaped_forms() {
8321 // Positive control sweep: every canonical-shaped tag from the
8322 // hello-rio / checkout-aplicacao / pangea-tatara-akeyless
8323 // example fixtures plus the substrate-fixed tags caixa-helm
8324 // unions in at chart render. Drift between this list and the
8325 // substrate-side `chart_keyword_shape_accepts_canonical_forms`
8326 // sweep surfaces here — one source of truth for the rule.
8327 let c = caixa_with_etiquetas(vec![
8328 "example",
8329 "aplicacao",
8330 "mesh",
8331 "ecommerce",
8332 "demo",
8333 "infrastructure",
8334 "aws",
8335 "akeyless",
8336 "pangea-native",
8337 "hello-world",
8338 "wasm",
8339 "rust",
8340 "tatara-lisp",
8341 "caixa-servico",
8342 "lareira",
8343 ]);
8344 c.validate_etiquetas().unwrap();
8345 }
8346
8347 // ── validate_autores — universal-axis maintainer shape ────────────
8348
8349 fn caixa_with_autores(autores: Vec<&str>) -> Caixa {
8350 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
8351 c.autores = autores.into_iter().map(String::from).collect();
8352 c
8353 }
8354
8355 #[test]
8356 fn validate_autores_accepts_empty_list() {
8357 // The empty-list identity: `Caixa::template` emits `:autores ()`,
8358 // so the gate is non-disruptive against every existing manifest.
8359 let c = caixa_with_autores(vec![]);
8360 c.validate_autores().unwrap();
8361 }
8362
8363 #[test]
8364 fn validate_autores_accepts_canonical_forms() {
8365 // Positive control sweep: every canonical-shaped non-empty
8366 // distinct maintainer list passes — the hello-rio / checkout-
8367 // aplicacao fixtures' `:autores ("pleme-io")` shape, plus the
8368 // multi-author shape downstream packaging surfaces emit.
8369 let c = caixa_with_autores(vec!["pleme-io"]);
8370 c.validate_autores().unwrap();
8371 let c = caixa_with_autores(vec!["alice <alice@example.com>", "bob <bob@example.com>"]);
8372 c.validate_autores().unwrap();
8373 }
8374
8375 #[test]
8376 fn validate_autores_rejects_empty_entry() {
8377 // Canonical paste-from-blank-doc footgun. Without the gate the
8378 // empty entry rendered as `maintainers: [{name: "", email: null}]`
8379 // in `Chart.yaml`, a no-op maintainer the substrate cannot route
8380 // to.
8381 let c = caixa_with_autores(vec![""]);
8382 let err = c.validate_autores().unwrap_err();
8383 assert!(matches!(err, ManifestError::AutorEmpty), "got {err:?}",);
8384 }
8385
8386 #[test]
8387 fn validate_autores_rejects_duplicate_entry() {
8388 // Canonical copy-paste-the-wrong-author footgun. Unlike the
8389 // `:etiquetas` peer (caixa-helm's `BTreeSet` collect silently
8390 // dedups the rendered `keywords:` array), the `maintainers:`
8391 // rendering has *no* dedup — duplicates stack verbatim. The
8392 // duplicate-arm names the offending author verbatim.
8393 let c = caixa_with_autores(vec!["pleme-io", "pleme-io"]);
8394 let err = c.validate_autores().unwrap_err();
8395 let ManifestError::AutorDuplicate { autor } = err else {
8396 panic!("expected AutorDuplicate, got {err:?}");
8397 };
8398 assert_eq!(autor, "pleme-io");
8399 }
8400
8401 #[test]
8402 fn validate_autores_empty_takes_precedence_over_duplicate() {
8403 // Empty-first cascade pin: `("" "pleme-io" "pleme-io")` surfaces
8404 // `AutorEmpty` not `AutorDuplicate` — the narrower structural
8405 // "this entry has no value" defect dominates the cross-entry
8406 // uniqueness diagnostic. Mirrors the peer empty-before-duplicate
8407 // cascades on `:etiquetas` (`EtiquetaEmpty` before
8408 // `EtiquetaDuplicate`, 360a499), `:caracteristicas`
8409 // (`CaracteristicaEmpty` before `CaracteristicaDuplicate`,
8410 // fc3b4d5), and `:membros :caixa` (`MembroCaixaEmpty` before
8411 // `MembroDuplicate`).
8412 let c = caixa_with_autores(vec!["", "pleme-io", "pleme-io"]);
8413 let err = c.validate_autores().unwrap_err();
8414 assert!(matches!(err, ManifestError::AutorEmpty), "got {err:?}",);
8415 }
8416
8417 #[test]
8418 fn validate_autores_duplicate_reports_first_collision() {
8419 // First-collision pin: `("a" "b" "a" "b")` surfaces the `"a"`
8420 // duplicate (the lexicographically-earliest offending position
8421 // — the second `"a"` at index 2 collides with the first `"a"`
8422 // at index 0), not the later `"b"` collision at index 3,
8423 // peer with every other first-collision diagnostic posture on
8424 // this surface.
8425 let c = caixa_with_autores(vec!["a", "b", "a", "b"]);
8426 let err = c.validate_autores().unwrap_err();
8427 let ManifestError::AutorDuplicate { autor } = err else {
8428 panic!("expected AutorDuplicate, got {err:?}");
8429 };
8430 assert_eq!(autor, "a");
8431 }
8432
8433 #[test]
8434 fn validate_autores_case_sensitive() {
8435 // Case-sensitivity pin: `("Pleme-io" "pleme-io")` is two distinct
8436 // entries, mirroring the peer `:etiquetas` / `:membros :caixa`
8437 // / `:children :caixa` exact-string-match discipline.
8438 let c = caixa_with_autores(vec!["Pleme-io", "pleme-io"]);
8439 c.validate_autores().unwrap();
8440 }
8441
8442 #[test]
8443 fn validate_autores_diagnostic_carries_offending_author() {
8444 // Diagnostic-shape pin (peer with
8445 // `validate_etiquetas_diagnostic_carries_offending_tag`): the
8446 // error's Display surfaces the offending author verbatim, so a
8447 // `feira lint` run can render the diagnostic without re-parsing
8448 // and the author can grep their caixa.lisp for the offending
8449 // value.
8450 let c = caixa_with_autores(vec!["pleme-io", "pleme-io"]);
8451 let rendered = c.validate_autores().unwrap_err().to_string();
8452 assert!(
8453 rendered.contains(":autores"),
8454 "diagnostic must name the offending slot: {rendered}",
8455 );
8456 assert!(
8457 rendered.contains("pleme-io"),
8458 "diagnostic must quote the offending author: {rendered}",
8459 );
8460 }
8461
8462 #[test]
8463 fn validate_autores_rejects_leading_whitespace_entry() {
8464 // Canonical paste-from-aligned-doc footgun. Without the shape
8465 // gate `" pleme-io"` silently passed validate and landed as a
8466 // YAML plain-style scalar with leading whitespace in the
8467 // rendered Chart.yaml `maintainers:` array — every YAML 1.2
8468 // dumper trims leading whitespace from plain-style scalars, so
8469 // the authored space round-tripped inconsistently back through
8470 // `caixa.lisp`. Mirrors the peer
8471 // `validate_descricao_rejects_leading_whitespace`.
8472 let c = caixa_with_autores(vec![" pleme-io"]);
8473 let err = c.validate_autores().unwrap_err();
8474 let ManifestError::AutorInvalid { autor, reason } = err else {
8475 panic!("expected AutorInvalid, got {err:?}");
8476 };
8477 assert_eq!(autor, " pleme-io");
8478 assert!(reason.contains("whitespace"), "got: {reason}");
8479 }
8480
8481 #[test]
8482 fn validate_autores_rejects_trailing_whitespace_entry() {
8483 // Canonical paste-from-doc footgun.
8484 let c = caixa_with_autores(vec!["pleme-io "]);
8485 let err = c.validate_autores().unwrap_err();
8486 let ManifestError::AutorInvalid { autor, reason } = err else {
8487 panic!("expected AutorInvalid, got {err:?}");
8488 };
8489 assert_eq!(autor, "pleme-io ");
8490 assert!(reason.contains("whitespace"), "got: {reason}");
8491 }
8492
8493 #[test]
8494 fn validate_autores_rejects_embedded_newline_entry() {
8495 // Canonical paste-from-multiline-doc footgun — the author
8496 // pasted a multi-line block of author records into one
8497 // `:autores` entry instead of splitting into one entry per
8498 // author. Without the shape gate `"alice\nbob"` silently
8499 // passed validate and landed as a YAML-illegal multi-line
8500 // scalar in the rendered Chart.yaml `maintainers:` array.
8501 let c = caixa_with_autores(vec!["alice\nbob"]);
8502 let err = c.validate_autores().unwrap_err();
8503 let ManifestError::AutorInvalid { autor, reason } = err else {
8504 panic!("expected AutorInvalid, got {err:?}");
8505 };
8506 assert_eq!(autor, "alice\nbob");
8507 assert!(reason.contains("newline"), "got: {reason}");
8508 }
8509
8510 #[test]
8511 fn validate_autores_rejects_embedded_carriage_return_entry() {
8512 // Canonical paste-from-Windows-CRLF-doc footgun.
8513 let c = caixa_with_autores(vec!["alice\rbob"]);
8514 let err = c.validate_autores().unwrap_err();
8515 let ManifestError::AutorInvalid { autor, reason } = err else {
8516 panic!("expected AutorInvalid, got {err:?}");
8517 };
8518 assert_eq!(autor, "alice\rbob");
8519 assert!(reason.contains("carriage return"), "got: {reason}");
8520 }
8521
8522 #[test]
8523 fn validate_autores_rejects_embedded_tab_entry() {
8524 // Canonical tab-from-aligned-doc footgun.
8525 let c = caixa_with_autores(vec!["Pleme\tContributors"]);
8526 let err = c.validate_autores().unwrap_err();
8527 let ManifestError::AutorInvalid { autor, reason } = err else {
8528 panic!("expected AutorInvalid, got {err:?}");
8529 };
8530 assert_eq!(autor, "Pleme\tContributors");
8531 assert!(reason.contains("tab"), "got: {reason}");
8532 }
8533
8534 #[test]
8535 fn validate_autores_rejects_embedded_control_bytes_entry() {
8536 // Paste-from-binary-blob footguns: NUL, BEL, ESC, DEL all
8537 // surface the same control-byte arm.
8538 for entry in [
8539 "alice\x00bob",
8540 "alice\x07bob",
8541 "alice\x1bbob",
8542 "alice\x7fbob",
8543 ] {
8544 let c = caixa_with_autores(vec![entry]);
8545 let err = c.validate_autores().unwrap_err();
8546 let ManifestError::AutorInvalid { autor, reason } = err else {
8547 panic!("expected AutorInvalid for {entry:?}, got {err:?}");
8548 };
8549 assert_eq!(autor, entry);
8550 assert!(
8551 reason.contains("control character"),
8552 "{entry:?} reason: {reason}",
8553 );
8554 }
8555 }
8556
8557 #[test]
8558 fn validate_autores_accepts_unicode_entry() {
8559 // Unicode positive control: realistic maintainer names carry
8560 // Unicode (`François`, `日本語`, `naïve`). The predicate must
8561 // round-trip Unicode losslessly, peer with the
8562 // `chart_maintainer_name_shape_accepts_unicode` substrate-side
8563 // sweep.
8564 let c = caixa_with_autores(vec![
8565 "François Dupont",
8566 "日本語の名前",
8567 "naïve <naive@example.com>",
8568 ]);
8569 c.validate_autores().unwrap();
8570 }
8571
8572 #[test]
8573 fn validate_autores_empty_takes_precedence_over_shape() {
8574 // Per-entry empty-first cascade pin: an entry that is both
8575 // empty *and* shape-invalid surfaces `AutorEmpty` (the narrower
8576 // "this entry has no value" structural defect dominates the
8577 // broader shape-predicate diagnostic). The empty arm fires
8578 // before the shape predicate is consulted, mirroring the peer
8579 // `validate_repositorio_empty_takes_precedence_over_shape`
8580 // cascade on the universal `Option<String>` siblings — and now
8581 // established on the Vec<String> per-entry surface.
8582 let c = caixa_with_autores(vec![""]);
8583 let err = c.validate_autores().unwrap_err();
8584 assert!(matches!(err, ManifestError::AutorEmpty), "got {err:?}",);
8585 }
8586
8587 #[test]
8588 fn validate_autores_shape_takes_precedence_over_duplicate() {
8589 // Per-entry shape-before-cross-entry-duplicate cascade pin: an
8590 // entry that is malformed surfaces `AutorInvalid` even when a
8591 // later entry would have collided on duplicate. The per-entry
8592 // shape arm fires inside the same loop iteration as the empty
8593 // arm, before the seen-set insert at end-of-iteration —
8594 // structural per-entry defects dominate the cross-entry
8595 // uniqueness diagnostic.
8596 let c = caixa_with_autores(vec!["alice\nbob", "alice\nbob"]);
8597 let err = c.validate_autores().unwrap_err();
8598 assert!(
8599 matches!(err, ManifestError::AutorInvalid { .. }),
8600 "got {err:?}",
8601 );
8602 }
8603
8604 #[test]
8605 fn validate_autores_invalid_diagnostic_names_offending_slot_and_value() {
8606 // Diagnostic-shape pin on the new shape arm (peer with
8607 // `validate_descricao_invalid_diagnostic_carries_offending_value`):
8608 // the rendered Display surfaces both the offending slot name
8609 // and the offending value verbatim, so a `feira lint` run
8610 // points the author at the exact `:autores` entry to fix.
8611 let c = caixa_with_autores(vec!["alice\nbob"]);
8612 let rendered = c.validate_autores().unwrap_err().to_string();
8613 assert!(
8614 rendered.contains(":autores"),
8615 "diagnostic must name the offending slot: {rendered}",
8616 );
8617 assert!(
8618 rendered.contains("alice\\nbob"),
8619 "diagnostic must quote the offending value (debug-escaped): {rendered}",
8620 );
8621 }
8622
8623 #[test]
8624 fn validate_autores_rejects_at_129_byte_boundary() {
8625 // The 128-byte cap pin — boundary-exceeding case rejected,
8626 // boundary-accepting case passes. Mirrors the peer
8627 // `chart_maintainer_name_shape_rejects_at_129_byte_boundary`
8628 // substrate-side pin, surfaced at the per-axis caller so the
8629 // cap propagates through validate end-to-end. Constructed as
8630 // a single all-`a` token so only the cap arm fires.
8631 let max_ok = "a".repeat(128);
8632 let c = caixa_with_autores(vec![max_ok.as_str()]);
8633 c.validate_autores().unwrap();
8634 let too_long = "a".repeat(129);
8635 let c = caixa_with_autores(vec![too_long.as_str()]);
8636 let err = c.validate_autores().unwrap_err();
8637 let ManifestError::AutorInvalid { reason, .. } = err else {
8638 panic!("expected AutorInvalid, got {err:?}");
8639 };
8640 assert!(reason.contains("128"), "got: {reason}");
8641 assert!(reason.contains("129"), "got: {reason}");
8642 }
8643
8644 // ── validate_repositorio — universal-axis git-repo-URL shape ──────
8645
8646 fn caixa_with_repositorio(repositorio: Option<&str>) -> Caixa {
8647 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
8648 c.repositorio = repositorio.map(String::from);
8649 c
8650 }
8651
8652 #[test]
8653 fn validate_repositorio_accepts_none() {
8654 // The omit-the-slot identity: `:repositorio` is optional. The
8655 // gate is a no-op when the author didn't declare a value —
8656 // every caixa without a `:repositorio` line trivially passes,
8657 // and the substrate-side renderers fall back to their
8658 // documented placeholder (`caixa-helm`'s `home: None`,
8659 // `caixa-flux`'s `https://github.com/pleme-io/<nome>` derived
8660 // URL). Mirrors the peer `validate_restart_window_accepts_none`
8661 // posture on the other `Option<String>` Caixa slot.
8662 let c = caixa_with_repositorio(None);
8663 c.validate_repositorio().unwrap();
8664 }
8665
8666 #[test]
8667 fn validate_repositorio_accepts_canonical_forms() {
8668 // Positive control sweep across every documented `:repositorio`
8669 // authoring shape — the same union the shared
8670 // `crate::render::is_git_repo_url` predicate accepts and the
8671 // peer `:deps :fonte :repo` axis already routes through.
8672 // Covers the `github:` shorthand (the canonical pleme-io
8673 // convention used in the `:repositorio` field of every
8674 // manifest fixture across `caixa-helm` / `caixa-mesh` and the
8675 // `examples/`), the `https://…` URL the README quickstart uses,
8676 // the `ssh://`, `git://`, `git@host:path` scp-style SSH, and
8677 // `file://` URL schemes the shared predicate documents.
8678 for repo in [
8679 "github:pleme-io/hello-rio",
8680 "github:pleme-io/checkout",
8681 "https://github.com/pleme-io/hello-rio",
8682 "ssh://git@github.com/pleme-io/hello-rio.git",
8683 "git://github.com/pleme-io/hello-rio.git",
8684 "git@github.com:pleme-io/hello-rio.git",
8685 "file:///srv/pleme/hello-rio",
8686 ] {
8687 let c = caixa_with_repositorio(Some(repo));
8688 c.validate_repositorio()
8689 .unwrap_or_else(|err| panic!("canonical {repo:?} must pass: {err:?}"));
8690 }
8691 }
8692
8693 #[test]
8694 fn validate_repositorio_rejects_empty_some() {
8695 // Canonical paste-from-blank-doc footgun. The narrower
8696 // [`ManifestError::RepositorioEmpty`] arm fires before the
8697 // shape predicate is consulted, mirroring the empty-first
8698 // cascade every peer per-axis identity gate uses
8699 // (`NomeEmpty` → `NomeInvalid`, `VersaoEmpty` → `VersaoInvalid`,
8700 // `FonteRepoEmpty` → `FonteRepoInvalid`). Without this gate
8701 // the empty `Some("")` silently passed the renderer's
8702 // `Option::unwrap_or_else(|| <fallback>)` (which only fires
8703 // on `None`) and landed as `home: ""` in `Chart.yaml` /
8704 // `url: ""` in the FluxCD `GitRepository`.
8705 let c = caixa_with_repositorio(Some(""));
8706 let err = c.validate_repositorio().unwrap_err();
8707 assert!(
8708 matches!(err, ManifestError::RepositorioEmpty),
8709 "got {err:?}",
8710 );
8711 }
8712
8713 #[test]
8714 fn validate_repositorio_rejects_whitespace() {
8715 // Paste-from-doc whitespace footgun. The shared
8716 // `is_git_repo_url` predicate refuses any whitespace byte; a
8717 // trailing space in a `:repositorio` value silently broke
8718 // `git clone '<value> '` at clone time. The diagnostic names
8719 // the offending value verbatim.
8720 let c = caixa_with_repositorio(Some("github:pleme-io/hello-rio "));
8721 let err = c.validate_repositorio().unwrap_err();
8722 let ManifestError::RepositorioInvalid { repositorio, .. } = err else {
8723 panic!("expected RepositorioInvalid, got {err:?}");
8724 };
8725 assert_eq!(repositorio, "github:pleme-io/hello-rio ");
8726 }
8727
8728 #[test]
8729 fn validate_repositorio_rejects_control_char() {
8730 // Paste-from-multiline-doc CRLF footgun — control characters
8731 // at the URL boundary are a class of subprocess-arg injection
8732 // and break git's URL parser at every porcelain entry point.
8733 let c = caixa_with_repositorio(Some("https://example.com/repo\n"));
8734 let err = c.validate_repositorio().unwrap_err();
8735 assert!(
8736 matches!(err, ManifestError::RepositorioInvalid { .. }),
8737 "got {err:?}",
8738 );
8739 }
8740
8741 #[test]
8742 fn validate_repositorio_rejects_leading_dash() {
8743 // Canonical CLI-argument-injection footgun: `git clone <repo>`
8744 // interprets a leading `-` as a CLI flag, so a
8745 // `-upload-pack=…` value escapes the subprocess argument
8746 // boundary. The shared predicate refuses every leading-`-`
8747 // shape at validate time.
8748 let c = caixa_with_repositorio(Some("-upload-pack=evil"));
8749 let err = c.validate_repositorio().unwrap_err();
8750 assert!(
8751 matches!(err, ManifestError::RepositorioInvalid { .. }),
8752 "got {err:?}",
8753 );
8754 }
8755
8756 #[test]
8757 fn validate_repositorio_rejects_missing_colon_separator() {
8758 // The bare `org/repo` ambiguity footgun — `git clone` reads
8759 // a no-`:` form as a relative filesystem path rather than the
8760 // GitHub-shorthand expansion the author probably intended.
8761 // The shared predicate refuses every shape without a `:`
8762 // separator.
8763 let c = caixa_with_repositorio(Some("pleme-io/hello-rio"));
8764 let err = c.validate_repositorio().unwrap_err();
8765 assert!(
8766 matches!(err, ManifestError::RepositorioInvalid { .. }),
8767 "got {err:?}",
8768 );
8769 }
8770
8771 #[test]
8772 fn validate_repositorio_rejects_fragment_anchor() {
8773 // Paste-from-browser-address-bar footgun on the
8774 // `:repositorio` axis — an author copies a GitHub permalink
8775 // to a README section / line-permalink and forgets to trim
8776 // the `#fragment` tail. The shared `is_git_repo_url`
8777 // predicate refuses the byte at the URL-grammar layer
8778 // (libcurl strips the fragment before opening the
8779 // transport, so the byte rides verbatim into the rendered
8780 // `Chart.yaml` `home:` and FluxCD `GitRepository` `url:`
8781 // fields but is silently dropped on the wire — two
8782 // manifest variants whose values differ only in their
8783 // fragment anchor lock to two distinct rendered artifacts
8784 // for the byte-identical clone, defeating the THEORY.md
8785 // §V.2 render-determinism contract on the `:repositorio`
8786 // axis the peer `:fonte :repo` axis already closes).
8787 let c = caixa_with_repositorio(Some("https://github.com/pleme-io/hello-rio#readme"));
8788 let err = c.validate_repositorio().unwrap_err();
8789 let ManifestError::RepositorioInvalid {
8790 repositorio,
8791 reason,
8792 } = err
8793 else {
8794 panic!("expected RepositorioInvalid, got {err:?}");
8795 };
8796 assert_eq!(repositorio, "https://github.com/pleme-io/hello-rio#readme");
8797 assert!(
8798 reason.contains("must not contain `#`"),
8799 "reason must surface the fragment-`#` arm, got {reason:?}"
8800 );
8801 }
8802
8803 #[test]
8804 fn validate_repositorio_rejects_query_string() {
8805 // Paste-from-browser-address-bar footgun on the
8806 // `:repositorio` axis (peer with the a68f818 fragment-`#`
8807 // arm on the same axis). An author copies a GitHub tab
8808 // deep-link out of the address bar and forgets to trim
8809 // the `?tab=…` query tail. The shared `is_git_repo_url`
8810 // predicate refuses the byte at the URL-grammar layer
8811 // (GitHub / GitLab / Bitbucket silently ignore the
8812 // `?query` tail and serve the same repo regardless, so
8813 // the byte rides verbatim into the rendered `Chart.yaml`
8814 // `home:` and FluxCD `GitRepository` `url:` fields but
8815 // is silently masked at the wire — two manifest variants
8816 // whose values differ only in their query tail lock to
8817 // two distinct rendered artifacts for the byte-identical
8818 // clone, defeating the THEORY.md §V.2 render-determinism
8819 // contract on the `:repositorio` axis the peer `:fonte
8820 // :repo` axis already closes).
8821 let c = caixa_with_repositorio(Some(
8822 "https://github.com/pleme-io/hello-rio?tab=readme-ov-file",
8823 ));
8824 let err = c.validate_repositorio().unwrap_err();
8825 let ManifestError::RepositorioInvalid {
8826 repositorio,
8827 reason,
8828 } = err
8829 else {
8830 panic!("expected RepositorioInvalid, got {err:?}");
8831 };
8832 assert_eq!(
8833 repositorio,
8834 "https://github.com/pleme-io/hello-rio?tab=readme-ov-file"
8835 );
8836 assert!(
8837 reason.contains("must not contain `?`"),
8838 "reason must surface the query-`?` arm, got {reason:?}"
8839 );
8840 }
8841
8842 #[test]
8843 fn validate_repositorio_rejects_embedded_backslash() {
8844 // Windows-file-path-confusion footgun on the `:repositorio`
8845 // axis (peer with the prior fragment-`#` / query-`?` arms on
8846 // the same axis, and peer with the new dep-level `:fonte :repo`
8847 // backslash arm on the URL-grammar trajectory). An author
8848 // pastes a Windows Explorer address-bar `file:///C:\Users\me\
8849 // hello-rio` into the `:repositorio` slot, expecting the
8850 // `lareira-<nome>` chart's `home:` field and the FluxCD
8851 // `GitRepository` `url:` field to render the canonical local
8852 // file-URI. The shared `is_git_repo_url` predicate refuses
8853 // the byte at the URL-grammar layer (libcurl silently
8854 // translates `\` → `/` on some platforms and refuses it on
8855 // others, so the byte rides verbatim into the rendered
8856 // artifacts but is silently rewritten or rejected at the wire
8857 // — two manifest variants whose values differ only in
8858 // backslash-vs-forward-slash lock to two distinct rendered
8859 // artifacts for the byte-identical clone, defeating the
8860 // THEORY.md §V.2 render-determinism contract on the
8861 // `:repositorio` axis the peer `:fonte :repo` axis already
8862 // closes).
8863 let c = caixa_with_repositorio(Some("file:///C:\\Users\\me\\hello-rio"));
8864 let err = c.validate_repositorio().unwrap_err();
8865 let ManifestError::RepositorioInvalid {
8866 repositorio,
8867 reason,
8868 } = err
8869 else {
8870 panic!("expected RepositorioInvalid, got {err:?}");
8871 };
8872 assert_eq!(repositorio, "file:///C:\\Users\\me\\hello-rio");
8873 assert!(
8874 reason.contains("must not contain `\\`"),
8875 "reason must surface the backslash-`\\` arm, got {reason:?}"
8876 );
8877 }
8878
8879 #[test]
8880 fn validate_repositorio_rejects_uri_template_placeholder() {
8881 // URI Template (RFC 6570) placeholder footgun on the
8882 // `:repositorio` axis (peer with the prior fragment-`#` /
8883 // query-`?` / backslash-`\` arms on the same axis, and peer
8884 // with the new dep-level `:fonte :repo` `{` / `}` arm on the
8885 // URL-grammar trajectory). An author pastes a quick-start
8886 // README snippet / OpenAPI `servers:` URL / Helm chart
8887 // `home:` template carrying unresolved `{org}` / `{repo}`
8888 // placeholders into the `:repositorio` slot, expecting the
8889 // substrate to resolve the placeholder downstream. The
8890 // shared `is_git_repo_url` predicate refuses the byte at the
8891 // URL-grammar layer (libcurl percent-encodes `{` / `}` to
8892 // `%7B` / `%7D` on the wire, so the byte round-trips
8893 // inconsistently between the rendered `Chart.yaml home:` /
8894 // FluxCD `GitRepository url:` and the resolver's `git clone`
8895 // invocation, defeating the THEORY.md §V.2 render-
8896 // determinism contract on the `:repositorio` axis the peer
8897 // `:fonte :repo` axis already closes; every git porcelain
8898 // entry-point additionally fetches a nonexistent literal-
8899 // `{placeholder}`-named path far from the source caixa.lisp).
8900 let c = caixa_with_repositorio(Some("https://github.com/{org}/hello-rio"));
8901 let err = c.validate_repositorio().unwrap_err();
8902 let ManifestError::RepositorioInvalid {
8903 repositorio,
8904 reason,
8905 } = err
8906 else {
8907 panic!("expected RepositorioInvalid, got {err:?}");
8908 };
8909 assert_eq!(repositorio, "https://github.com/{org}/hello-rio");
8910 assert!(
8911 reason.contains("must not contain `{`"),
8912 "reason must surface the open-brace `{{` arm, got {reason:?}"
8913 );
8914 assert!(
8915 reason.contains("URI Template") || reason.contains("RFC 6570"),
8916 "reason must name the RFC 6570 URI Template grammar, got {reason:?}"
8917 );
8918 }
8919
8920 #[test]
8921 fn validate_repositorio_empty_takes_precedence_over_shape() {
8922 // Empty-first cascade pin: the empty `Some("")` surfaces the
8923 // narrower `RepositorioEmpty` not the shape-predicate-wrapped
8924 // `RepositorioInvalid`, mirroring the peer
8925 // `NomeEmpty` → `NomeInvalid`, `VersaoEmpty` → `VersaoInvalid`,
8926 // `FonteRepoEmpty` → `FonteRepoInvalid` cascades. The shared
8927 // `is_git_repo_url` predicate also rejects the empty input
8928 // (defensively, with its own `"must not be empty"` reason),
8929 // but the manifest-layer empty arm runs first to surface the
8930 // narrower diagnostic verbatim.
8931 let c = caixa_with_repositorio(Some(""));
8932 let err = c.validate_repositorio().unwrap_err();
8933 assert!(
8934 matches!(err, ManifestError::RepositorioEmpty),
8935 "got {err:?}",
8936 );
8937 }
8938
8939 #[test]
8940 fn validate_repositorio_diagnostic_carries_offending_value() {
8941 // Diagnostic-shape pin (peer with
8942 // `validate_autores_diagnostic_carries_offending_author`): the
8943 // error's Display surfaces the offending value + slot name
8944 // verbatim, so a `feira lint` run can render the diagnostic
8945 // without re-parsing and the author can grep their caixa.lisp
8946 // for the offending `:repositorio` value.
8947 let c = caixa_with_repositorio(Some("pleme-io/hello-rio"));
8948 let rendered = c.validate_repositorio().unwrap_err().to_string();
8949 assert!(
8950 rendered.contains(":repositorio"),
8951 "diagnostic must name the offending slot: {rendered}",
8952 );
8953 assert!(
8954 rendered.contains("pleme-io/hello-rio"),
8955 "diagnostic must quote the offending value: {rendered}",
8956 );
8957 }
8958
8959 // ── validate_descricao — universal-axis Chart.yaml description shape ──
8960
8961 fn caixa_with_descricao(descricao: Option<&str>) -> Caixa {
8962 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
8963 c.descricao = descricao.map(String::from);
8964 c
8965 }
8966
8967 #[test]
8968 fn validate_descricao_accepts_none() {
8969 // The omit-the-slot identity: `:descricao` is optional. The
8970 // gate is a no-op when the author didn't declare a value —
8971 // every caixa without a `:descricao` line trivially passes,
8972 // and the substrate-side renderers fall back to their
8973 // documented `caixa.nome`-derived placeholder. Mirrors the
8974 // peer `validate_repositorio_accepts_none` posture on the
8975 // sibling `Option<String>` Caixa slot.
8976 let c = caixa_with_descricao(None);
8977 c.validate_descricao().unwrap();
8978 }
8979
8980 #[test]
8981 fn validate_descricao_accepts_canonical_summary() {
8982 // Positive control: the canonical pleme-io descricao shape —
8983 // a short free-form prose summary — passes the gate. Covers
8984 // the fixture shapes the `caixa-helm` / `caixa-flux` /
8985 // `caixa-mesh` test fixtures use (`"Canonical Rust→wasm32-
8986 // wasip2 caixa Servico."`, `"Checkout flow."`).
8987 for desc in [
8988 "Canonical Rust→wasm32-wasip2 caixa Servico.",
8989 "Checkout flow.",
8990 "AWS provider caixa for tatara-lisp",
8991 "FIXME — describe this caixa",
8992 "x",
8993 ] {
8994 let c = caixa_with_descricao(Some(desc));
8995 c.validate_descricao()
8996 .unwrap_or_else(|err| panic!("canonical {desc:?} must pass: {err:?}"));
8997 }
8998 }
8999
9000 #[test]
9001 fn validate_descricao_rejects_empty_some() {
9002 // Canonical paste-from-blank-doc footgun. Without this gate
9003 // the empty `Some("")` silently passed the renderer's
9004 // `Option::unwrap_or_else(|| <fallback>)` (which only fires
9005 // on `None`) and landed as `description: ""` in `Chart.yaml`
9006 // and a blank `README.md` header. Mirrors the peer
9007 // [`ManifestError::RepositorioEmpty`] empty-arm on the
9008 // sibling `Option<String>` Caixa slot.
9009 let c = caixa_with_descricao(Some(""));
9010 let err = c.validate_descricao().unwrap_err();
9011 assert!(matches!(err, ManifestError::DescricaoEmpty), "got {err:?}",);
9012 }
9013
9014 #[test]
9015 fn validate_descricao_rejects_leading_whitespace() {
9016 // Paste-from-aligned-doc footgun: a leading ASCII space the
9017 // bare empty-arm gate accepted, the shape predicate now
9018 // refuses. The diagnostic carries the offending value
9019 // verbatim (with the leading space preserved) so the author
9020 // can grep their caixa.lisp for the exact `:descricao` line
9021 // and fix the round-trip-inconsistent leading whitespace.
9022 // Mirrors the peer
9023 // `validate_licenca_rejects_leading_whitespace` arm on the
9024 // sibling `:licenca` axis.
9025 let c = caixa_with_descricao(Some(" Checkout flow."));
9026 let err = c.validate_descricao().unwrap_err();
9027 let ManifestError::DescricaoInvalid { descricao, reason } = err else {
9028 panic!("expected DescricaoInvalid, got {err:?}");
9029 };
9030 assert_eq!(descricao, " Checkout flow.");
9031 assert!(reason.contains("whitespace"), "got: {reason:?}");
9032 }
9033
9034 #[test]
9035 fn validate_descricao_rejects_trailing_whitespace() {
9036 // Paste-from-doc footgun: a trailing ASCII space the bare
9037 // empty-arm gate accepted, the shape predicate now refuses.
9038 let c = caixa_with_descricao(Some("Checkout flow. "));
9039 let err = c.validate_descricao().unwrap_err();
9040 let ManifestError::DescricaoInvalid { descricao, reason } = err else {
9041 panic!("expected DescricaoInvalid, got {err:?}");
9042 };
9043 assert_eq!(descricao, "Checkout flow. ");
9044 assert!(reason.contains("whitespace"), "got: {reason:?}");
9045 }
9046
9047 #[test]
9048 fn validate_descricao_rejects_embedded_newline() {
9049 // Paste-from-multiline-doc footgun: an embedded LF the bare
9050 // empty-arm gate accepted, the shape predicate now refuses.
9051 // Without this gate the embedded newline silently landed in
9052 // the rendered Chart.yaml as a multi-line YAML block scalar,
9053 // and every chart-aware UI (`helm list`, `helm search`,
9054 // Artifact Hub) renders the description in a single-line
9055 // column so the embedded newline is silently dropped at
9056 // every downstream consumer.
9057 let c = caixa_with_descricao(Some("Checkout\nflow."));
9058 let err = c.validate_descricao().unwrap_err();
9059 assert!(
9060 matches!(err, ManifestError::DescricaoInvalid { .. }),
9061 "got {err:?}",
9062 );
9063 assert!(err.to_string().contains("newline"), "got {err}");
9064 }
9065
9066 #[test]
9067 fn validate_descricao_rejects_embedded_carriage_return() {
9068 // Paste-from-Windows-CRLF-doc footgun.
9069 let c = caixa_with_descricao(Some("Checkout\rflow."));
9070 let err = c.validate_descricao().unwrap_err();
9071 assert!(
9072 matches!(err, ManifestError::DescricaoInvalid { .. }),
9073 "got {err:?}",
9074 );
9075 assert!(err.to_string().contains("carriage return"), "got {err}");
9076 }
9077
9078 #[test]
9079 fn validate_descricao_rejects_embedded_tab() {
9080 // Tab-from-aligned-doc footgun.
9081 let c = caixa_with_descricao(Some("Checkout\tflow."));
9082 let err = c.validate_descricao().unwrap_err();
9083 assert!(
9084 matches!(err, ManifestError::DescricaoInvalid { .. }),
9085 "got {err:?}",
9086 );
9087 assert!(err.to_string().contains("tab"), "got {err}");
9088 }
9089
9090 #[test]
9091 fn validate_descricao_rejects_embedded_control_bytes() {
9092 // Paste-from-binary-blob footgun: every other control byte
9093 // (NUL, BEL, ESC, DEL) is refused at validate time. Mirrors
9094 // the peer SPDX-expression control-byte arm.
9095 for s in [
9096 "Checkout\x00flow.",
9097 "Checkout\x07flow.",
9098 "Checkout\x1bflow.",
9099 "Checkout\x7fflow.",
9100 ] {
9101 let c = caixa_with_descricao(Some(s));
9102 let err = c.validate_descricao().unwrap_err();
9103 assert!(
9104 matches!(err, ManifestError::DescricaoInvalid { .. }),
9105 "{s:?} got {err:?}",
9106 );
9107 assert!(
9108 err.to_string().contains("control character"),
9109 "{s:?} got {err}",
9110 );
9111 }
9112 }
9113
9114 #[test]
9115 fn validate_descricao_accepts_unicode_prose() {
9116 // Positive control: Unicode prose is accepted — the
9117 // canonical fixtures carry `→` (U+2192) and `—` (U+2014),
9118 // and `Caixa::template`'s `"FIXME — describe this caixa"`
9119 // scaffold every `feira init` emits must continue to pass.
9120 for s in [
9121 "Canonical Rust→wasm32-wasip2 caixa Servico.",
9122 "FIXME — describe this caixa",
9123 "Caixa pour le projet tâche",
9124 "日本語の説明",
9125 ] {
9126 let c = caixa_with_descricao(Some(s));
9127 c.validate_descricao()
9128 .unwrap_or_else(|err| panic!("Unicode {s:?} must pass: {err:?}"));
9129 }
9130 }
9131
9132 #[test]
9133 fn validate_descricao_empty_takes_precedence_over_shape() {
9134 // Cascade pin: a `Some("")` surfaces the narrower
9135 // `DescricaoEmpty` arm, not the broader `DescricaoInvalid`
9136 // shape-predicate arm. Mirrors the peer
9137 // `validate_licenca_empty_takes_precedence_over_shape` pin
9138 // on the sibling `:licenca` axis.
9139 let c = caixa_with_descricao(Some(""));
9140 let err = c.validate_descricao().unwrap_err();
9141 assert!(matches!(err, ManifestError::DescricaoEmpty), "got {err:?}",);
9142 }
9143
9144 #[test]
9145 fn validate_descricao_invalid_diagnostic_carries_offending_value_and_slot() {
9146 // Diagnostic-shape pin: the error's Display surfaces both
9147 // the `:descricao` slot name and the offending value
9148 // verbatim, so a `feira lint` run can render the diagnostic
9149 // without re-parsing and the author can grep their caixa.lisp
9150 // for the offending `:descricao` line. Mirrors the peer
9151 // `validate_licenca_invalid_diagnostic_carries_offending_value_and_slot`
9152 // pin (ee2e888) on the sibling `:licenca` axis.
9153 // The `{descricao:?}` Debug format escapes embedded control
9154 // bytes; the quoted offending value surfaces as
9155 // `"Checkout\nflow."` (literal backslash-n) in the rendered
9156 // diagnostic. The author can grep their caixa.lisp for the
9157 // literal `Checkout` summary prefix.
9158 let c = caixa_with_descricao(Some("Checkout\nflow."));
9159 let rendered = c.validate_descricao().unwrap_err().to_string();
9160 assert!(
9161 rendered.contains(":descricao"),
9162 "diagnostic must name the offending slot: {rendered}",
9163 );
9164 assert!(
9165 rendered.contains("Checkout\\nflow."),
9166 "diagnostic must quote the offending value (debug-escaped): {rendered}",
9167 );
9168 }
9169
9170 #[test]
9171 fn validate_descricao_template_passes() {
9172 // Round-trip pin: the bare `Caixa::template` shape carries
9173 // `:descricao "FIXME — describe this caixa"` (a non-empty
9174 // sentinel), so the template-derived Caixa passes the gate by
9175 // construction. A future template-shape change that omits or
9176 // empties `:descricao` would surface here as a regression.
9177 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
9178 c.validate_descricao().unwrap();
9179 }
9180
9181 #[test]
9182 fn validate_descricao_diagnostic_names_offending_slot() {
9183 // Diagnostic-shape pin (peer with
9184 // `validate_repositorio_diagnostic_carries_offending_value`):
9185 // the error's Display surfaces the `:descricao` slot name
9186 // verbatim, so a `feira lint` run can render the diagnostic
9187 // without re-parsing and the author can grep their caixa.lisp
9188 // for the offending `:descricao` line.
9189 let c = caixa_with_descricao(Some(""));
9190 let rendered = c.validate_descricao().unwrap_err().to_string();
9191 assert!(
9192 rendered.contains(":descricao"),
9193 "diagnostic must name the offending slot: {rendered}",
9194 );
9195 }
9196
9197 // ── validate_licenca — universal-axis chart README license shape ──
9198
9199 fn caixa_with_licenca(licenca: Option<&str>) -> Caixa {
9200 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
9201 c.licenca = licenca.map(String::from);
9202 c
9203 }
9204
9205 #[test]
9206 fn validate_licenca_accepts_none() {
9207 // The omit-the-slot identity: `:licenca` is optional. The
9208 // gate is a no-op when the author didn't declare a value —
9209 // every caixa without a `:licenca` line trivially passes,
9210 // and the substrate-side `caixa-helm` renderer falls back to
9211 // the documented `"MIT"` placeholder. Mirrors the peer
9212 // `validate_descricao_accepts_none` posture on the sibling
9213 // `Option<String>` Caixa slot.
9214 let c = caixa_with_licenca(None);
9215 c.validate_licenca().unwrap();
9216 }
9217
9218 #[test]
9219 fn validate_licenca_accepts_canonical_expressions() {
9220 // Positive control: every canonical SPDX expression shape
9221 // pleme-io carries in its existing fixtures + the canonical
9222 // SPDX dual-license / with-exception / `+`-suffix / grouped /
9223 // user-defined-reference shapes all pass the gate. Covers
9224 // the single-license, `OR`-compound, `AND`-compound,
9225 // `WITH`-exception, parenthesis-grouped, `+`-suffix, and
9226 // `LicenseRef-` / `DocumentRef-:LicenseRef-` shapes — every
9227 // production the SPDX 2.1 expression grammar admits that
9228 // sits within the alphabet floor the
9229 // `is_spdx_expression_shape` predicate enforces.
9230 for lic in [
9231 "MIT",
9232 "Apache-2.0",
9233 "Apache-2.0 OR MIT",
9234 "Apache-2.0 AND MIT",
9235 "BSD-3-Clause",
9236 "MPL-2.0",
9237 "GPL-3.0-or-later",
9238 "GPL-2.0+",
9239 "Apache-2.0 WITH LLVM-exception",
9240 "(MIT OR Apache-2.0) AND BSD-3-Clause",
9241 "(MIT OR Apache-2.0) AND BSD-3-Clause AND ISC",
9242 "LicenseRef-MyLicense",
9243 "DocumentRef-spdx-tool:LicenseRef-MIT-Style",
9244 "x",
9245 ] {
9246 let c = caixa_with_licenca(Some(lic));
9247 c.validate_licenca()
9248 .unwrap_or_else(|err| panic!("canonical {lic:?} must pass: {err:?}"));
9249 }
9250 }
9251
9252 #[test]
9253 fn validate_licenca_rejects_trailing_whitespace() {
9254 // Paste-from-doc whitespace footgun. A trailing space in the
9255 // `:licenca` value would silently break a downstream SPDX
9256 // parser that splits on exact `AND` / `OR` / `WITH` keyword
9257 // boundaries. The shape predicate refuses every trailing
9258 // whitespace byte by construction. Peer with
9259 // `validate_repositorio_rejects_whitespace` and
9260 // `validate_edicao_rejects_trailing_whitespace`.
9261 let c = caixa_with_licenca(Some("MIT "));
9262 let err = c.validate_licenca().unwrap_err();
9263 let ManifestError::LicencaInvalid { licenca, .. } = err else {
9264 panic!("expected LicencaInvalid, got {err:?}");
9265 };
9266 assert_eq!(licenca, "MIT ");
9267 }
9268
9269 #[test]
9270 fn validate_licenca_rejects_leading_whitespace() {
9271 // Symmetric paste-from-doc whitespace footgun on the leading
9272 // boundary — the gate refuses every shape that starts with a
9273 // space byte by construction. Peer with
9274 // `validate_edicao_rejects_leading_whitespace`.
9275 let c = caixa_with_licenca(Some(" MIT"));
9276 let err = c.validate_licenca().unwrap_err();
9277 assert!(
9278 matches!(err, ManifestError::LicencaInvalid { .. }),
9279 "got {err:?}",
9280 );
9281 }
9282
9283 #[test]
9284 fn validate_licenca_rejects_control_char() {
9285 // Paste-from-multiline-doc CRLF footgun — control characters
9286 // at the value boundary land as a malformed line in the
9287 // rendered chart `README.md` `## License` section. Peer with
9288 // `validate_repositorio_rejects_control_char` and
9289 // `validate_edicao_rejects_control_char`.
9290 for lic in ["MIT\n", "MIT\r\n", "MIT\rApache-2.0"] {
9291 let c = caixa_with_licenca(Some(lic));
9292 let err = c.validate_licenca().unwrap_err();
9293 assert!(
9294 matches!(err, ManifestError::LicencaInvalid { .. }),
9295 "expected LicencaInvalid on {lic:?}, got {err:?}",
9296 );
9297 }
9298 }
9299
9300 #[test]
9301 fn validate_licenca_rejects_tab() {
9302 // Tab-from-aligned-doc footgun — SPDX expressions use a
9303 // single ASCII space between tokens; a tab breaks every
9304 // downstream SPDX parser that splits on exact `" "`
9305 // boundaries.
9306 let c = caixa_with_licenca(Some("MIT\tOR Apache-2.0"));
9307 let err = c.validate_licenca().unwrap_err();
9308 assert!(
9309 matches!(err, ManifestError::LicencaInvalid { .. }),
9310 "got {err:?}",
9311 );
9312 }
9313
9314 #[test]
9315 fn validate_licenca_rejects_non_ascii() {
9316 // Smart-quote / non-ASCII paste footgun — SPDX identifiers
9317 // are ASCII per the `idstring = 1*(ALPHA / DIGIT / "-" /
9318 // ".")` production. The shape predicate refuses every
9319 // non-ASCII byte by construction; peer with
9320 // `validate_edicao_rejects_non_ascii_lookalike`.
9321 for lic in ["MIT\u{a0}OR Apache-2.0", "MIT\u{2013}1.0", "Café-1.0"] {
9322 let c = caixa_with_licenca(Some(lic));
9323 let err = c.validate_licenca().unwrap_err();
9324 assert!(
9325 matches!(err, ManifestError::LicencaInvalid { .. }),
9326 "expected LicencaInvalid on {lic:?}, got {err:?}",
9327 );
9328 }
9329 }
9330
9331 #[test]
9332 fn validate_licenca_rejects_underscore() {
9333 // Underscore-instead-of-hyphen typo footgun — `Apache_2.0` /
9334 // `MIT_Style` / `BSD_3_Clause` are familiar shapes from
9335 // snake-case identifier conventions that don't apply to the
9336 // SPDX `idstring` grammar (which admits only `ALPHA / DIGIT /
9337 // "-" / "."`). The shape predicate refuses every underscore
9338 // byte by construction.
9339 for lic in ["Apache_2.0", "MIT_Style", "BSD_3_Clause"] {
9340 let c = caixa_with_licenca(Some(lic));
9341 let err = c.validate_licenca().unwrap_err();
9342 assert!(
9343 matches!(err, ManifestError::LicencaInvalid { .. }),
9344 "expected LicencaInvalid on {lic:?}, got {err:?}",
9345 );
9346 }
9347 }
9348
9349 #[test]
9350 fn validate_licenca_rejects_comma_separator() {
9351 // Comma-instead-of-`OR`-keyword colloquial idiom footgun —
9352 // SPDX expressions compose multiple licenses via `AND` / `OR`
9353 // keywords, not the comma separator. The shape predicate
9354 // refuses every comma byte by construction.
9355 for lic in ["MIT, Apache-2.0", "MIT,Apache-2.0"] {
9356 let c = caixa_with_licenca(Some(lic));
9357 let err = c.validate_licenca().unwrap_err();
9358 assert!(
9359 matches!(err, ManifestError::LicencaInvalid { .. }),
9360 "expected LicencaInvalid on {lic:?}, got {err:?}",
9361 );
9362 }
9363 }
9364
9365 #[test]
9366 fn validate_licenca_rejects_slash_dual_license() {
9367 // Slash-dual-license colloquial idiom footgun — the
9368 // `MIT/Apache-2.0` shape is common in Cargo's pre-SPDX
9369 // `package.license` field but non-SPDX; the SPDX equivalent
9370 // is `MIT OR Apache-2.0`. The shape predicate refuses every
9371 // forward-slash byte by construction.
9372 for lic in ["MIT/Apache-2.0", "MIT/BSD-3-Clause"] {
9373 let c = caixa_with_licenca(Some(lic));
9374 let err = c.validate_licenca().unwrap_err();
9375 assert!(
9376 matches!(err, ManifestError::LicencaInvalid { .. }),
9377 "expected LicencaInvalid on {lic:?}, got {err:?}",
9378 );
9379 }
9380 }
9381
9382 #[test]
9383 fn validate_licenca_rejects_semicolon_separator() {
9384 // Semicolon-list-separator confusion footgun — adjacent to
9385 // the comma-separator idiom, every list-separator-belongs-
9386 // to-list-grammar confusion lands here.
9387 let c = caixa_with_licenca(Some("MIT; Apache-2.0"));
9388 let err = c.validate_licenca().unwrap_err();
9389 assert!(
9390 matches!(err, ManifestError::LicencaInvalid { .. }),
9391 "got {err:?}",
9392 );
9393 }
9394
9395 #[test]
9396 fn validate_licenca_empty_takes_precedence_over_shape() {
9397 // Empty-first cascade pin: the empty `Some("")` surfaces the
9398 // narrower `LicencaEmpty` not the shape-predicate-wrapped
9399 // `LicencaInvalid`, mirroring the peer
9400 // `validate_edicao_empty_takes_precedence_over_shape` and
9401 // `validate_repositorio_empty_takes_precedence_over_shape`
9402 // (`RepositorioEmpty` → `RepositorioInvalid`), `NomeEmpty` →
9403 // `NomeInvalid`, `VersaoEmpty` → `VersaoInvalid` cascades.
9404 // The shape predicate also refuses the empty input
9405 // (defensively — `"must not be empty"`), but the manifest-
9406 // layer empty arm runs first to surface the narrower
9407 // diagnostic verbatim.
9408 let c = caixa_with_licenca(Some(""));
9409 let err = c.validate_licenca().unwrap_err();
9410 assert!(matches!(err, ManifestError::LicencaEmpty), "got {err:?}",);
9411 }
9412
9413 #[test]
9414 fn validate_licenca_invalid_diagnostic_carries_offending_value() {
9415 // Diagnostic-shape pin on the shape-predicate arm (peer with
9416 // `validate_edicao_invalid_diagnostic_carries_offending_value`
9417 // and `validate_repositorio_diagnostic_carries_offending_value`):
9418 // the error's Display surfaces the offending value + slot
9419 // name verbatim, so a `feira lint` run can render the
9420 // diagnostic without re-parsing and the author can grep
9421 // their caixa.lisp for the offending `:licenca` value.
9422 let c = caixa_with_licenca(Some("Apache_2.0"));
9423 let rendered = c.validate_licenca().unwrap_err().to_string();
9424 assert!(
9425 rendered.contains(":licenca"),
9426 "diagnostic must name the offending slot: {rendered}",
9427 );
9428 assert!(
9429 rendered.contains("Apache_2.0"),
9430 "diagnostic must quote the offending value: {rendered}",
9431 );
9432 }
9433
9434 #[test]
9435 fn validate_licenca_rejects_empty_some() {
9436 // Canonical paste-from-blank-doc footgun. Without this gate
9437 // the empty `Some("")` silently passed the renderer's
9438 // `Option::unwrap_or_else(|| "MIT".into())` (which only
9439 // fires on `None`) and landed as a bare trailing period in
9440 // the rendered chart `README.md` `## License` section.
9441 // Mirrors the peer [`ManifestError::DescricaoEmpty`] empty-
9442 // arm on the sibling `Option<String>` Caixa slot.
9443 let c = caixa_with_licenca(Some(""));
9444 let err = c.validate_licenca().unwrap_err();
9445 assert!(matches!(err, ManifestError::LicencaEmpty), "got {err:?}",);
9446 }
9447
9448 #[test]
9449 fn validate_licenca_template_passes() {
9450 // Round-trip pin: the bare `Caixa::template` shape (whether
9451 // it carries `:licenca` or omits it) passes the gate by
9452 // construction. A future template-shape change that
9453 // introduced `(:licenca "")` would surface here as a
9454 // regression. Mirrors the peer
9455 // `validate_descricao_template_passes` pin.
9456 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
9457 c.validate_licenca().unwrap();
9458 }
9459
9460 #[test]
9461 fn validate_licenca_diagnostic_names_offending_slot() {
9462 // Diagnostic-shape pin (peer with
9463 // `validate_descricao_diagnostic_names_offending_slot`):
9464 // the error's Display surfaces the `:licenca` slot name
9465 // verbatim, so a `feira lint` run can render the diagnostic
9466 // without re-parsing and the author can grep their caixa.lisp
9467 // for the offending `:licenca` line.
9468 let c = caixa_with_licenca(Some(""));
9469 let rendered = c.validate_licenca().unwrap_err().to_string();
9470 assert!(
9471 rendered.contains(":licenca"),
9472 "diagnostic must name the offending slot: {rendered}",
9473 );
9474 }
9475
9476 // ── Caixa::licenca — outer top-level Option<&str> scalar accessor ──
9477
9478 #[test]
9479 fn licenca_returns_licenca_byte_string_verbatim_across_permutations() {
9480 // The canonical per-`Caixa` `:licenca` SPDX-expression scalar
9481 // pin: [`Caixa::licenca`] must return the `:licenca` typed
9482 // byte-string verbatim as an `Option<&str>`, byte-equal to the
9483 // raw `self.licenca.as_deref()` access across every
9484 // representative value in the accept-set — `None` (the "omit
9485 // the slot to defer to the caixa-helm renderer's `MIT`
9486 // fallback" arm every existing fixture without a `:licenca`
9487 // line carries), `Some("")` (a past-the-guard sentinel that
9488 // pins the accessor doesn't perform a silent
9489 // `Some("") → None` collapse on the empty arm — validate
9490 // rejects `Some("")` through `LicencaEmpty` but the accessor
9491 // must ship the raw slot verbatim so a validate-time gate
9492 // regression surfaces at the caixa-helm emit boundary rather
9493 // than being silently absorbed into the fallback), `Some("MIT")`
9494 // (the canonical single-license shape every `feira init`
9495 // template scaffolds), `Some("Apache-2.0 OR MIT")` (the
9496 // canonical `OR`-compound shape the peer
9497 // `validate_licenca_accepts_canonical_expressions` positive
9498 // sweep exercises), `Some("(MIT OR Apache-2.0) AND
9499 // BSD-3-Clause")` (the canonical parenthesis-grouped shape),
9500 // `Some("MIT ")` / `Some(" MIT")` / `Some("MIT\n")` /
9501 // `Some("Apache_2.0")` / `Some("MIT,Apache-2.0")` (past-the-
9502 // guard sentinels — validate rejects each through
9503 // `LicencaInvalid` but the accessor must ship the raw slot
9504 // verbatim).
9505 //
9506 // First outer top-level [`Caixa`] `Option<&str>`-return scalar
9507 // accessor pin on the substrate primitive — opens the "outer
9508 // [`Caixa`] `Option<&str>` scalar" projection pattern the
9509 // sibling per-`Caixa` `:descricao` / `:repositorio` / `:edicao`
9510 // future lifts fold on. Sibling in shape to the peer per-`:placement`
9511 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
9512 // [`crate::aplicacao::Placement::affinity`] (74ec2d3) accessor
9513 // pins on the sibling per-M3-mesh-slot `Option<&str>`-return
9514 // axes, extended onto the outer top-level [`Caixa`] universal-
9515 // axis surface. Pins against a future silent detour that
9516 // returned an owned `Option<String>` (which would type-check
9517 // but silently allocate on every accessor call, breaking the
9518 // zero-cost projection every peer sibling accessor carries), a
9519 // `Some("") → None` collapse (which would silently absorb the
9520 // `LicencaEmpty` refusal case at the accessor boundary and the
9521 // caixa-helm emit path would silently fall back to `"MIT"` on
9522 // a struct-literal `Caixa { licenca: Some(""), .. }`), or a
9523 // `None → Some("MIT")` collapse (which would silently reify
9524 // the caixa-helm renderer's `"MIT"` fallback at the accessor
9525 // boundary and every downstream consumer keying off the
9526 // `Option::is_none()` discriminator would lose the "author
9527 // omitted the slot" signal).
9528 for licenca in [
9529 None,
9530 Some(""),
9531 Some("MIT"),
9532 Some("Apache-2.0 OR MIT"),
9533 Some("(MIT OR Apache-2.0) AND BSD-3-Clause"),
9534 Some("MIT "),
9535 Some(" MIT"),
9536 Some("MIT\n"),
9537 Some("Apache_2.0"),
9538 Some("MIT,Apache-2.0"),
9539 ] {
9540 let c = caixa_with_licenca(licenca);
9541 assert_eq!(
9542 c.licenca(),
9543 licenca,
9544 "Caixa::licenca must return :licenca verbatim (got {:?}, \
9545 expected {licenca:?})",
9546 c.licenca(),
9547 );
9548 assert_eq!(
9549 c.licenca(),
9550 c.licenca.as_deref(),
9551 "Caixa::licenca must byte-equal the raw \
9552 `self.licenca.as_deref()` field access across every \
9553 value in the Option<&str> accept-set",
9554 );
9555 }
9556 }
9557
9558 #[test]
9559 fn validate_licenca_empty_arm_routes_through_accessor() {
9560 // Composition pin: [`Caixa::validate_licenca`]'s empty-arm gate
9561 // must key off [`Caixa::licenca`], not the raw
9562 // `self.licenca.as_deref()` field access. Structurally: a
9563 // `Caixa { licenca: Some(""), .. }` must surface the
9564 // `LicencaEmpty` refusal exactly, and a
9565 // `Caixa { licenca: Some("MIT"), .. }` (the canonical
9566 // single-license form) must pass validate. The pair jointly
9567 // pins the accessor + validate-gate composition: any future
9568 // silent detour that had the accessor return `None` on the
9569 // empty arm (a `.filter(|s| !s.is_empty())` collapse) would
9570 // silently absorb the `LicencaEmpty` refusal at the accessor
9571 // boundary and the validate gate would accept a struct-literal
9572 // `Caixa { licenca: Some(""), .. }` — the composition pin
9573 // catches that at caixa-core build time.
9574 //
9575 // Peer of the per-`:politicas :circuit-breaker`
9576 // [`crate::aplicacao::CircuitBreaker::max_failures`] (3a74062)
9577 // accessor-composition pin
9578 // (`validate_politicas_max_failures_zero_floor_arm_routes_through_accessor`)
9579 // on the sibling per-M3-mesh-slot required-`u32` axis — same
9580 // "the validate / shape-gate predicate must route through the
9581 // substrate-primitive typed dispatch" discipline extended onto
9582 // the outer top-level [`Caixa`] universal-axis
9583 // `Option<&str>`-composition surface.
9584 let c = caixa_with_licenca(Some(""));
9585 assert!(
9586 matches!(c.validate_licenca(), Err(ManifestError::LicencaEmpty)),
9587 "validate_licenca must reject licenca == Some(\"\") with \
9588 LicencaEmpty — the accessor and the validate gate must \
9589 route through the same substrate-primitive typed dispatch \
9590 on the :licenca empty arm",
9591 );
9592 let c = caixa_with_licenca(Some("MIT"));
9593 assert!(
9594 c.validate_licenca().is_ok(),
9595 "validate_licenca must accept licenca == Some(\"MIT\") \
9596 (the canonical single-license SPDX shape)",
9597 );
9598 }
9599
9600 #[test]
9601 fn licenca_projects_option_str_by_borrow() {
9602 // The by-borrow pin: [`Caixa::licenca`] returns
9603 // `Option<&str>` by borrow — the `&str` borrows the underlying
9604 // `String` storage of the `Option<String>` slot and the
9605 // accessor must not allocate a fresh `String` on every call.
9606 // Peer of the per-`:placement`
9607 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) by-
9608 // borrow pin on the peer per-M3-mesh-slot
9609 // `Option<&str>`-return axis, extended onto the outer top-
9610 // level [`Caixa`] universal-axis `Option<&str>` shape — the
9611 // accessor's returned `&str` must borrow from `&self` (the
9612 // returned reference's lifetime is tied to `&self`), and
9613 // calling the accessor twice on the same [`Caixa`] must yield
9614 // the same `Option<&str>` verbatim (idempotent, no side
9615 // effects on `&self`).
9616 //
9617 // Pins against a future silent detour that returned an owned
9618 // `Option<String>` (which would type-check but silently
9619 // allocate on every call, breaking the zero-cost projection
9620 // every peer sibling accessor carries), or a one-arm-only
9621 // accessor that returned a saturating value on some sentinel
9622 // input (breaking the pass-through invariant the sibling
9623 // required-scalar accessors carry).
9624 for licenca in [None, Some(""), Some("MIT"), Some("Apache-2.0 OR MIT")] {
9625 let c = caixa_with_licenca(licenca);
9626 let first = c.licenca();
9627 let second = c.licenca();
9628 assert_eq!(
9629 first, second,
9630 "Caixa::licenca must be idempotent — two successive \
9631 calls on the same &self must return the same \
9632 Option<&str>",
9633 );
9634 assert_eq!(
9635 first, licenca,
9636 "Caixa::licenca must return :licenca verbatim by \
9637 borrow — got {first:?}, expected {licenca:?}",
9638 );
9639 }
9640 }
9641
9642 // ── Caixa::repositorio — outer top-level Option<&str> scalar accessor ──
9643
9644 #[test]
9645 fn repositorio_returns_repositorio_byte_string_verbatim_across_permutations() {
9646 // The canonical per-`Caixa` `:repositorio` git-repo-URL scalar
9647 // pin: [`Caixa::repositorio`] must return the `:repositorio`
9648 // typed byte-string verbatim as an `Option<&str>`, byte-equal
9649 // to the raw `self.repositorio.as_deref()` access across every
9650 // representative value in the accept-set — `None` (the "omit
9651 // the slot to defer to the per-renderer placeholder" arm every
9652 // existing fixture without a `:repositorio` line carries),
9653 // `Some("")` (a past-the-guard sentinel that pins the accessor
9654 // doesn't perform a silent `Some("") → None` collapse on the
9655 // empty arm — validate rejects `Some("")` through
9656 // `RepositorioEmpty` but the accessor must ship the raw slot
9657 // verbatim so a validate-time gate regression surfaces at the
9658 // caixa-helm / caixa-flux emit boundary rather than being
9659 // silently absorbed into the per-renderer fallback),
9660 // `Some("github:pleme-io/hello-rio")` (the canonical `github:`
9661 // shorthand every existing manifest fixture across
9662 // `caixa-helm` / `caixa-mesh` and the `examples/` uses),
9663 // `Some("https://github.com/pleme-io/checkout")` (the canonical
9664 // `https://` URL the README quickstart uses),
9665 // `Some("ssh://git@github.com/pleme-io/checkout.git")` /
9666 // `Some("git://github.com/pleme-io/checkout.git")` /
9667 // `Some("git@github.com:pleme-io/checkout.git")` /
9668 // `Some("file:///opt/mirrors/pleme-io/checkout")` (every non-
9669 // github scheme the shared `is_git_repo_url` predicate
9670 // documents), and five past-the-guard sentinels for the
9671 // `RepositorioInvalid` refusal cases (`Some("pleme-io/checkout")`
9672 // missing-colon, `Some("-upload-pack=evil")` leading-dash, /
9673 // `Some("github:pleme-io/checkout?ref=main")` query-string, /
9674 // `Some("github:pleme-io/checkout#main")` fragment-anchor, /
9675 // `Some("github:pleme-io/{tpl}")` URI-template-placeholder — the
9676 // sentinels pin the accessor doesn't silently absorb the
9677 // refusal cases into a fallback).
9678 //
9679 // Second outer top-level [`Caixa`] `Option<&str>`-return scalar
9680 // accessor pin on the substrate primitive — sibling of the peer
9681 // [`Caixa::licenca`] (6d5bc28) pin
9682 // (`licenca_returns_licenca_byte_string_verbatim_across_permutations`)
9683 // that opened the "outer [`Caixa`] `Option<&str>` scalar"
9684 // projection pin pattern this pin folds on. Sibling in shape to
9685 // the peer per-`:placement`
9686 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
9687 // [`crate::aplicacao::Placement::affinity`] (74ec2d3) accessor
9688 // pins on the sibling per-M3-mesh-slot `Option<&str>`-return
9689 // axes, extended onto the outer top-level [`Caixa`] universal-
9690 // axis surface. Pins against a future silent detour that
9691 // returned an owned `Option<String>` (which would type-check
9692 // but silently allocate on every accessor call, breaking the
9693 // zero-cost projection every peer sibling accessor carries), a
9694 // `Some("") → None` collapse (which would silently absorb the
9695 // `RepositorioEmpty` refusal case at the accessor boundary and
9696 // the caixa-helm `Chart.yaml` `home:` fold would silently
9697 // render a `home: null` / omitted field on a struct-literal
9698 // `Caixa { repositorio: Some(""), .. }`), or a
9699 // `None → Some(<default>)` collapse (which would silently reify
9700 // the per-renderer fallback at the accessor boundary and every
9701 // downstream consumer keying off the `Option::is_none()`
9702 // discriminator would lose the "author omitted the slot"
9703 // signal).
9704 for repositorio in [
9705 None,
9706 Some(""),
9707 Some("github:pleme-io/hello-rio"),
9708 Some("https://github.com/pleme-io/checkout"),
9709 Some("ssh://git@github.com/pleme-io/checkout.git"),
9710 Some("git://github.com/pleme-io/checkout.git"),
9711 Some("git@github.com:pleme-io/checkout.git"),
9712 Some("file:///opt/mirrors/pleme-io/checkout"),
9713 Some("pleme-io/checkout"),
9714 Some("-upload-pack=evil"),
9715 Some("github:pleme-io/checkout?ref=main"),
9716 Some("github:pleme-io/checkout#main"),
9717 Some("github:pleme-io/{tpl}"),
9718 ] {
9719 let c = caixa_with_repositorio(repositorio);
9720 assert_eq!(
9721 c.repositorio(),
9722 repositorio,
9723 "Caixa::repositorio must return :repositorio verbatim \
9724 (got {:?}, expected {repositorio:?})",
9725 c.repositorio(),
9726 );
9727 assert_eq!(
9728 c.repositorio(),
9729 c.repositorio.as_deref(),
9730 "Caixa::repositorio must byte-equal the raw \
9731 `self.repositorio.as_deref()` field access across every \
9732 value in the Option<&str> accept-set",
9733 );
9734 }
9735 }
9736
9737 #[test]
9738 fn validate_repositorio_empty_arm_routes_through_accessor() {
9739 // Composition pin: [`Caixa::validate_repositorio`]'s empty-arm
9740 // gate must key off [`Caixa::repositorio`], not the raw
9741 // `self.repositorio.as_deref()` field access. Structurally: a
9742 // `Caixa { repositorio: Some(""), .. }` must surface the
9743 // `RepositorioEmpty` refusal exactly, and a
9744 // `Caixa { repositorio: Some("github:pleme-io/hello-rio"), .. }`
9745 // (the canonical `github:` shorthand form) must pass validate.
9746 // The pair jointly pins the accessor + validate-gate
9747 // composition: any future silent detour that had the accessor
9748 // return `None` on the empty arm (a `.filter(|s| !s.is_empty())`
9749 // collapse) would silently absorb the `RepositorioEmpty` refusal
9750 // at the accessor boundary and the validate gate would accept a
9751 // struct-literal `Caixa { repositorio: Some(""), .. }` — the
9752 // composition pin catches that at caixa-core build time.
9753 //
9754 // Peer of the [`Caixa::licenca`] (6d5bc28)
9755 // `validate_licenca_empty_arm_routes_through_accessor`
9756 // composition pin on the sibling outer top-level [`Caixa`]
9757 // `Option<&str>` universal-axis surface — same "the validate /
9758 // shape-gate predicate must route through the substrate-
9759 // primitive typed dispatch" discipline extended onto the second
9760 // outer top-level [`Caixa`] universal-axis `Option<&str>`-
9761 // composition surface.
9762 let c = caixa_with_repositorio(Some(""));
9763 assert!(
9764 matches!(
9765 c.validate_repositorio(),
9766 Err(ManifestError::RepositorioEmpty),
9767 ),
9768 "validate_repositorio must reject repositorio == Some(\"\") \
9769 with RepositorioEmpty — the accessor and the validate gate \
9770 must route through the same substrate-primitive typed \
9771 dispatch on the :repositorio empty arm",
9772 );
9773 let c = caixa_with_repositorio(Some("github:pleme-io/hello-rio"));
9774 assert!(
9775 c.validate_repositorio().is_ok(),
9776 "validate_repositorio must accept repositorio == \
9777 Some(\"github:pleme-io/hello-rio\") (the canonical \
9778 `github:` shorthand git-repo-URL shape)",
9779 );
9780 }
9781
9782 #[test]
9783 fn repositorio_projects_option_str_by_borrow() {
9784 // The by-borrow pin: [`Caixa::repositorio`] returns
9785 // `Option<&str>` by borrow — the `&str` borrows the underlying
9786 // `String` storage of the `Option<String>` slot and the
9787 // accessor must not allocate a fresh `String` on every call.
9788 // Peer of the per-`:placement`
9789 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) and the
9790 // [`Caixa::licenca`] (6d5bc28) by-borrow pins on the peer
9791 // `Option<&str>`-return axes, extended onto the second outer
9792 // top-level [`Caixa`] universal-axis `Option<&str>` shape —
9793 // the accessor's returned `&str` must borrow from `&self` (the
9794 // returned reference's lifetime is tied to `&self`), and
9795 // calling the accessor twice on the same [`Caixa`] must yield
9796 // the same `Option<&str>` verbatim (idempotent, no side effects
9797 // on `&self`).
9798 //
9799 // Pins against a future silent detour that returned an owned
9800 // `Option<String>` (which would type-check but silently
9801 // allocate on every call, breaking the zero-cost projection
9802 // every peer sibling accessor carries), or a one-arm-only
9803 // accessor that returned a saturating value on some sentinel
9804 // input (breaking the pass-through invariant the sibling
9805 // required-scalar accessors carry).
9806 for repositorio in [
9807 None,
9808 Some(""),
9809 Some("github:pleme-io/hello-rio"),
9810 Some("https://github.com/pleme-io/checkout"),
9811 ] {
9812 let c = caixa_with_repositorio(repositorio);
9813 let first = c.repositorio();
9814 let second = c.repositorio();
9815 assert_eq!(
9816 first, second,
9817 "Caixa::repositorio must be idempotent — two successive \
9818 calls on the same &self must return the same \
9819 Option<&str>",
9820 );
9821 assert_eq!(
9822 first, repositorio,
9823 "Caixa::repositorio must return :repositorio verbatim by \
9824 borrow — got {first:?}, expected {repositorio:?}",
9825 );
9826 }
9827 }
9828
9829 // ── Caixa::descricao — outer top-level Option<&str> scalar accessor ──
9830
9831 #[test]
9832 fn descricao_returns_descricao_byte_string_verbatim_across_permutations() {
9833 // The canonical per-`Caixa` `:descricao` free-form-prose scalar
9834 // pin: [`Caixa::descricao`] must return the `:descricao` typed
9835 // byte-string verbatim as an `Option<&str>`, byte-equal to the
9836 // raw `self.descricao.as_deref()` access across every
9837 // representative value in the accept-set — `None` (the "omit
9838 // the slot to defer to the per-renderer `caixa.nome`-derived
9839 // fallback" arm every existing fixture without a `:descricao`
9840 // line carries), `Some("")` (a past-the-guard sentinel that
9841 // pins the accessor doesn't perform a silent `Some("") → None`
9842 // collapse on the empty arm — validate rejects `Some("")`
9843 // through `DescricaoEmpty` but the accessor must ship the raw
9844 // slot verbatim so a validate-time gate regression surfaces at
9845 // the caixa-helm / caixa-feira emit boundary rather than being
9846 // silently absorbed into the per-renderer `caixa.nome`-derived
9847 // fallback), `Some("Checkout flow.")` (the canonical one-line
9848 // prose descriptor the peer
9849 // `validate_descricao_accepts_canonical_value` positive sweep
9850 // exercises), `Some("Canonical Rust→wasm32-wasip2 caixa
9851 // Servico.")` (the multi-byte Unicode continuation-byte shape
9852 // the `hello-rio` fixture carries), `Some("→ — · ✓")` (a
9853 // multi-glyph Unicode shape the peer
9854 // `is_chart_description_shape` predicate accepts), and five
9855 // past-the-guard sentinels for the `DescricaoInvalid` refusal
9856 // cases (`Some(" Checkout flow.")` leading-whitespace,
9857 // `Some("Checkout flow. ")` trailing-whitespace,
9858 // `Some("Checkout\nflow.")` embedded-LF,
9859 // `Some("Checkout\tflow.")` embedded-TAB, and
9860 // `Some("Checkout\x00flow.")` embedded-NUL — the sentinels pin
9861 // the accessor doesn't silently absorb the refusal cases into
9862 // a fallback).
9863 //
9864 // Third outer top-level [`Caixa`] `Option<&str>`-return scalar
9865 // accessor pin on the substrate primitive — sibling of the peer
9866 // [`Caixa::licenca`] (6d5bc28) and [`Caixa::repositorio`]
9867 // (cc7332d) pins that opened the "outer [`Caixa`]
9868 // `Option<&str>` scalar" projection pin pattern this pin folds
9869 // on. Sibling in shape to the peer per-`:placement`
9870 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
9871 // [`crate::aplicacao::Placement::affinity`] (74ec2d3) accessor
9872 // pins on the sibling per-M3-mesh-slot `Option<&str>`-return
9873 // axes, extended onto the outer top-level [`Caixa`] universal-
9874 // axis surface. Pins against a future silent detour that
9875 // returned an owned `Option<String>` (which would type-check
9876 // but silently allocate on every accessor call, breaking the
9877 // zero-cost projection every peer sibling accessor carries), a
9878 // `Some("") → None` collapse (which would silently absorb the
9879 // `DescricaoEmpty` refusal case at the accessor boundary and
9880 // the caixa-helm `Chart.yaml` `description:` fold would
9881 // silently render a `caixa.nome`-derived fallback on a
9882 // struct-literal `Caixa { descricao: Some(""), .. }`), or a
9883 // `None → Some(<default>)` collapse (which would silently
9884 // reify the per-renderer `caixa.nome`-derived fallback at the
9885 // accessor boundary and every downstream consumer keying off
9886 // the `Option::is_none()` discriminator would lose the "author
9887 // omitted the slot" signal).
9888 for descricao in [
9889 None,
9890 Some(""),
9891 Some("Checkout flow."),
9892 Some("Canonical Rust→wasm32-wasip2 caixa Servico."),
9893 Some("→ — · ✓"),
9894 Some(" Checkout flow."),
9895 Some("Checkout flow. "),
9896 Some("Checkout\nflow."),
9897 Some("Checkout\tflow."),
9898 Some("Checkout\x00flow."),
9899 ] {
9900 let c = caixa_with_descricao(descricao);
9901 assert_eq!(
9902 c.descricao(),
9903 descricao,
9904 "Caixa::descricao must return :descricao verbatim (got \
9905 {:?}, expected {descricao:?})",
9906 c.descricao(),
9907 );
9908 assert_eq!(
9909 c.descricao(),
9910 c.descricao.as_deref(),
9911 "Caixa::descricao must byte-equal the raw \
9912 `self.descricao.as_deref()` field access across every \
9913 value in the Option<&str> accept-set",
9914 );
9915 }
9916 }
9917
9918 #[test]
9919 fn validate_descricao_empty_arm_routes_through_accessor() {
9920 // Composition pin: [`Caixa::validate_descricao`]'s empty-arm
9921 // gate must key off [`Caixa::descricao`], not the raw
9922 // `self.descricao.as_deref()` field access. Structurally: a
9923 // `Caixa { descricao: Some(""), .. }` must surface the
9924 // `DescricaoEmpty` refusal exactly, and a
9925 // `Caixa { descricao: Some("Checkout flow."), .. }` (the
9926 // canonical one-line-prose form) must pass validate. The pair
9927 // jointly pins the accessor + validate-gate composition: any
9928 // future silent detour that had the accessor return `None` on
9929 // the empty arm (a `.filter(|s| !s.is_empty())` collapse) would
9930 // silently absorb the `DescricaoEmpty` refusal at the accessor
9931 // boundary and the validate gate would accept a struct-literal
9932 // `Caixa { descricao: Some(""), .. }` — the composition pin
9933 // catches that at caixa-core build time.
9934 //
9935 // Peer of the [`Caixa::licenca`] (6d5bc28)
9936 // `validate_licenca_empty_arm_routes_through_accessor` and
9937 // [`Caixa::repositorio`] (cc7332d)
9938 // `validate_repositorio_empty_arm_routes_through_accessor`
9939 // composition pins on the sibling outer top-level [`Caixa`]
9940 // `Option<&str>` universal-axis surface — same "the validate /
9941 // shape-gate predicate must route through the substrate-
9942 // primitive typed dispatch" discipline extended onto the third
9943 // outer top-level [`Caixa`] universal-axis `Option<&str>`-
9944 // composition surface.
9945 let c = caixa_with_descricao(Some(""));
9946 assert!(
9947 matches!(c.validate_descricao(), Err(ManifestError::DescricaoEmpty),),
9948 "validate_descricao must reject descricao == Some(\"\") \
9949 with DescricaoEmpty — the accessor and the validate gate \
9950 must route through the same substrate-primitive typed \
9951 dispatch on the :descricao empty arm",
9952 );
9953 let c = caixa_with_descricao(Some("Checkout flow."));
9954 assert!(
9955 c.validate_descricao().is_ok(),
9956 "validate_descricao must accept descricao == \
9957 Some(\"Checkout flow.\") (the canonical one-line-prose \
9958 chart-description shape)",
9959 );
9960 }
9961
9962 #[test]
9963 fn descricao_projects_option_str_by_borrow() {
9964 // The by-borrow pin: [`Caixa::descricao`] returns
9965 // `Option<&str>` by borrow — the `&str` borrows the underlying
9966 // `String` storage of the `Option<String>` slot and the
9967 // accessor must not allocate a fresh `String` on every call.
9968 // Peer of the [`Caixa::licenca`] (6d5bc28) and
9969 // [`Caixa::repositorio`] (cc7332d) by-borrow pins on the peer
9970 // outer top-level [`Caixa`] `Option<&str>`-return axes, and of
9971 // the per-`:placement`
9972 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) by-
9973 // borrow pin on the peer per-M3-mesh-slot `Option<&str>`-
9974 // return axis, extended onto the third outer top-level
9975 // [`Caixa`] universal-axis `Option<&str>` shape — the
9976 // accessor's returned `&str` must borrow from `&self` (the
9977 // returned reference's lifetime is tied to `&self`), and
9978 // calling the accessor twice on the same [`Caixa`] must yield
9979 // the same `Option<&str>` verbatim (idempotent, no side
9980 // effects on `&self`).
9981 //
9982 // Pins against a future silent detour that returned an owned
9983 // `Option<String>` (which would type-check but silently
9984 // allocate on every call, breaking the zero-cost projection
9985 // every peer sibling accessor carries), or a one-arm-only
9986 // accessor that returned a saturating value on some sentinel
9987 // input (breaking the pass-through invariant the sibling
9988 // required-scalar accessors carry).
9989 for descricao in [
9990 None,
9991 Some(""),
9992 Some("Checkout flow."),
9993 Some("Canonical Rust→wasm32-wasip2 caixa Servico."),
9994 ] {
9995 let c = caixa_with_descricao(descricao);
9996 let first = c.descricao();
9997 let second = c.descricao();
9998 assert_eq!(
9999 first, second,
10000 "Caixa::descricao must be idempotent — two successive \
10001 calls on the same &self must return the same \
10002 Option<&str>",
10003 );
10004 assert_eq!(
10005 first, descricao,
10006 "Caixa::descricao must return :descricao verbatim by \
10007 borrow — got {first:?}, expected {descricao:?}",
10008 );
10009 }
10010 }
10011
10012 // ── validate_edicao — universal-axis language-edition shape ──
10013
10014 fn caixa_with_edicao(edicao: Option<&str>) -> Caixa {
10015 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
10016 c.edicao = edicao.map(String::from);
10017 c
10018 }
10019
10020 #[test]
10021 fn validate_edicao_accepts_none() {
10022 // The omit-the-slot identity: `:edicao` is optional. The
10023 // gate is a no-op when the author didn't declare a value —
10024 // every caixa without an `:edicao` line trivially passes,
10025 // and the substrate-side build pipeline falls back to the
10026 // documented default edition. Mirrors the peer
10027 // `validate_licenca_accepts_none` posture on the sibling
10028 // `Option<String>` Caixa slot.
10029 let c = caixa_with_edicao(None);
10030 c.validate_edicao().unwrap();
10031 }
10032
10033 #[test]
10034 fn validate_edicao_accepts_canonical_value() {
10035 // Positive control: the canonical `"2026"` edition every
10036 // existing renderer-side fixture (`caixa-helm`, `caixa-flux`,
10037 // `caixa-mesh`) carries by construction passes the gate.
10038 // Future-introduced sibling editions (`"2027"`, `"2030"`,
10039 // `"2049"`) that match the same 4-digit ASCII decimal year
10040 // shape must also trivially pass — the structural shape
10041 // predicate accepts every well-formed year regardless of
10042 // whether the substrate yet understands the specific value
10043 // (a future known-edition allowlist tightens that).
10044 for ed in ["2026", "2027", "2030", "2049"] {
10045 let c = caixa_with_edicao(Some(ed));
10046 c.validate_edicao()
10047 .unwrap_or_else(|err| panic!("canonical {ed:?} must pass: {err:?}"));
10048 }
10049 }
10050
10051 #[test]
10052 fn validate_edicao_rejects_empty_some() {
10053 // Canonical paste-from-blank-doc footgun. Without this gate
10054 // the empty `Some("")` silently lands as `(:edicao "")` in
10055 // the rendered caixa.lisp and a future renderer-side
10056 // consumer's `Option::unwrap_or_else` (which only fires on
10057 // `None`) skips its fallback. Mirrors the peer
10058 // [`ManifestError::LicencaEmpty`] empty-arm on the sibling
10059 // `Option<String>` Caixa slot.
10060 let c = caixa_with_edicao(Some(""));
10061 let err = c.validate_edicao().unwrap_err();
10062 assert!(matches!(err, ManifestError::EdicaoEmpty), "got {err:?}",);
10063 }
10064
10065 #[test]
10066 fn validate_edicao_rejects_free_form_non_year() {
10067 // Free-form non-year footgun: the bare `"x"` / `"latest"` /
10068 // `"nightly"` shapes carry no operational meaning on the
10069 // substrate's build-time edition selector. Until this gate
10070 // landed the bare empty-arm check let every such value
10071 // through and broke far from the source caixa.lisp. Peer
10072 // with the shape-predicate cascade
10073 // `validate_repositorio_rejects_missing_colon_separator`
10074 // establishes past its own empty arm.
10075 for ed in ["x", "latest", "nightly", "stable"] {
10076 let c = caixa_with_edicao(Some(ed));
10077 let err = c.validate_edicao().unwrap_err();
10078 assert!(
10079 matches!(err, ManifestError::EdicaoInvalid { .. }),
10080 "expected EdicaoInvalid on {ed:?}, got {err:?}",
10081 );
10082 }
10083 }
10084
10085 #[test]
10086 fn validate_edicao_rejects_trailing_whitespace() {
10087 // Paste-from-doc whitespace footgun. A trailing space in
10088 // the `:edicao` value would silently break the substrate's
10089 // build-time edition match-table lookup at the rendered
10090 // artifact's edition-selector consumer. The shape predicate
10091 // refuses every whitespace byte by construction (any byte
10092 // outside `0-9` fails `is_ascii_digit`). Peer with
10093 // `validate_repositorio_rejects_whitespace`.
10094 let c = caixa_with_edicao(Some("2026 "));
10095 let err = c.validate_edicao().unwrap_err();
10096 let ManifestError::EdicaoInvalid { edicao, .. } = err else {
10097 panic!("expected EdicaoInvalid, got {err:?}");
10098 };
10099 assert_eq!(edicao, "2026 ");
10100 }
10101
10102 #[test]
10103 fn validate_edicao_rejects_leading_whitespace() {
10104 // Symmetric paste-from-doc whitespace footgun on the leading
10105 // boundary — the gate refuses every shape with a non-digit
10106 // byte by construction.
10107 let c = caixa_with_edicao(Some(" 2026"));
10108 let err = c.validate_edicao().unwrap_err();
10109 assert!(
10110 matches!(err, ManifestError::EdicaoInvalid { .. }),
10111 "got {err:?}",
10112 );
10113 }
10114
10115 #[test]
10116 fn validate_edicao_rejects_control_char() {
10117 // Paste-from-multiline-doc CRLF footgun — control characters
10118 // at the value boundary break the substrate's build-time
10119 // edition-selector parser. Peer with
10120 // `validate_repositorio_rejects_control_char`.
10121 let c = caixa_with_edicao(Some("2026\n"));
10122 let err = c.validate_edicao().unwrap_err();
10123 assert!(
10124 matches!(err, ManifestError::EdicaoInvalid { .. }),
10125 "got {err:?}",
10126 );
10127 }
10128
10129 #[test]
10130 fn validate_edicao_rejects_non_ascii_lookalike() {
10131 // Fullwidth-keyboard look-alike footgun — `"2026"` is
10132 // the U+FF12 U+FF10 U+FF12 U+FF16 sequence (CJK fullwidth
10133 // digits), 4 codepoints but 12 UTF-8 bytes; the substrate's
10134 // edition selector wants an ASCII year, and the gate
10135 // refuses every non-ASCII shape by construction (length in
10136 // bytes is 12 ≠ 4, *and* every byte falls outside
10137 // `is_ascii_digit`'s `0-9` range).
10138 let c = caixa_with_edicao(Some("2026"));
10139 let err = c.validate_edicao().unwrap_err();
10140 assert!(
10141 matches!(err, ManifestError::EdicaoInvalid { .. }),
10142 "got {err:?}",
10143 );
10144 }
10145
10146 #[test]
10147 fn validate_edicao_rejects_version_tag_prefix() {
10148 // Common version-tag idiom footgun — `"v2026"` / `"e2026"`
10149 // / `"r2026"` are familiar shapes from git-tag / Rust
10150 // edition / release-tag conventions that don't apply to
10151 // the year-shaped edition axis. The shape predicate refuses
10152 // every leading non-digit prefix.
10153 for ed in ["v2026", "e2026", "r2026"] {
10154 let c = caixa_with_edicao(Some(ed));
10155 let err = c.validate_edicao().unwrap_err();
10156 assert!(
10157 matches!(err, ManifestError::EdicaoInvalid { .. }),
10158 "expected EdicaoInvalid on {ed:?}, got {err:?}",
10159 );
10160 }
10161 }
10162
10163 #[test]
10164 fn validate_edicao_rejects_decimal_shape() {
10165 // Decimal-shaped pseudo-version footgun — `"2026.1"` /
10166 // `"2026.0"` are familiar shapes from semver / float
10167 // conventions that don't apply to the year-shaped edition
10168 // axis. The shape predicate refuses every non-digit byte
10169 // (`.` falls outside `is_ascii_digit`).
10170 for ed in ["2026.1", "2026.0", "2026.0.1"] {
10171 let c = caixa_with_edicao(Some(ed));
10172 let err = c.validate_edicao().unwrap_err();
10173 assert!(
10174 matches!(err, ManifestError::EdicaoInvalid { .. }),
10175 "expected EdicaoInvalid on {ed:?}, got {err:?}",
10176 );
10177 }
10178 }
10179
10180 #[test]
10181 fn validate_edicao_rejects_wrong_length_numeric() {
10182 // Wrong-length numeric footgun — `"26"` (truncated) /
10183 // `"202"` (truncated) / `"20260"` (extra digit) / `"00026"`
10184 // (zero-padded too wide) all parse as integers but don't
10185 // name a 4-digit year. The shape predicate refuses every
10186 // value whose length isn't exactly 4 bytes.
10187 for ed in ["26", "202", "20260", "00026", "9"] {
10188 let c = caixa_with_edicao(Some(ed));
10189 let err = c.validate_edicao().unwrap_err();
10190 assert!(
10191 matches!(err, ManifestError::EdicaoInvalid { .. }),
10192 "expected EdicaoInvalid on {ed:?}, got {err:?}",
10193 );
10194 }
10195 }
10196
10197 #[test]
10198 fn validate_edicao_empty_takes_precedence_over_shape() {
10199 // Empty-first cascade pin: the empty `Some("")` surfaces
10200 // the narrower `EdicaoEmpty` not the shape-predicate-
10201 // wrapped `EdicaoInvalid`, mirroring the peer
10202 // `validate_repositorio_empty_takes_precedence_over_shape`
10203 // (`RepositorioEmpty` → `RepositorioInvalid`),
10204 // `NomeEmpty` → `NomeInvalid`, `VersaoEmpty` →
10205 // `VersaoInvalid`, `FonteRepoEmpty` → `FonteRepoInvalid`
10206 // cascades. The shape predicate also refuses the empty
10207 // input (defensively — `s.len() != 4`), but the
10208 // manifest-layer empty arm runs first to surface the
10209 // narrower diagnostic verbatim.
10210 let c = caixa_with_edicao(Some(""));
10211 let err = c.validate_edicao().unwrap_err();
10212 assert!(matches!(err, ManifestError::EdicaoEmpty), "got {err:?}",);
10213 }
10214
10215 #[test]
10216 fn validate_edicao_template_passes() {
10217 // Round-trip pin: the bare `Caixa::template` shape (which
10218 // carries `:edicao "2026"` verbatim) passes the gate by
10219 // construction. A future template-shape change that
10220 // introduced `(:edicao "")` or a non-year value would
10221 // surface here as a regression. Mirrors the peer
10222 // `validate_licenca_template_passes` pin.
10223 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
10224 c.validate_edicao().unwrap();
10225 }
10226
10227 #[test]
10228 fn validate_edicao_diagnostic_names_offending_slot() {
10229 // Diagnostic-shape pin (peer with
10230 // `validate_licenca_diagnostic_names_offending_slot`): the
10231 // error's Display surfaces the `:edicao` slot name verbatim,
10232 // so a `feira lint` run can render the diagnostic without
10233 // re-parsing and the author can grep their caixa.lisp for
10234 // the offending `:edicao` line.
10235 let c = caixa_with_edicao(Some(""));
10236 let rendered = c.validate_edicao().unwrap_err().to_string();
10237 assert!(
10238 rendered.contains(":edicao"),
10239 "diagnostic must name the offending slot: {rendered}",
10240 );
10241 }
10242
10243 #[test]
10244 fn validate_edicao_invalid_diagnostic_carries_offending_value() {
10245 // Diagnostic-shape pin on the shape-predicate arm (peer
10246 // with `validate_repositorio_diagnostic_carries_offending_value`):
10247 // the error's Display surfaces the offending value + slot
10248 // name verbatim, so a `feira lint` run can render the
10249 // diagnostic without re-parsing and the author can grep
10250 // their caixa.lisp for the offending `:edicao` value.
10251 let c = caixa_with_edicao(Some("v2026"));
10252 let rendered = c.validate_edicao().unwrap_err().to_string();
10253 assert!(
10254 rendered.contains(":edicao"),
10255 "diagnostic must name the offending slot: {rendered}",
10256 );
10257 assert!(
10258 rendered.contains("v2026"),
10259 "diagnostic must quote the offending value: {rendered}",
10260 );
10261 }
10262
10263 // ── Caixa::edicao — outer top-level Option<&str> scalar accessor ──
10264
10265 #[test]
10266 fn edicao_returns_edicao_byte_string_verbatim_across_permutations() {
10267 // The canonical per-`Caixa` `:edicao` language-edition scalar
10268 // pin: [`Caixa::edicao`] must return the `:edicao` typed
10269 // byte-string verbatim as an `Option<&str>`, byte-equal to the
10270 // raw `self.edicao.as_deref()` access across every representative
10271 // value in the accept-set — `None` (the "omit the slot to defer
10272 // to the substrate's default edition" arm every existing
10273 // [`caixa-resolver`] fixture without an `:edicao` line carries),
10274 // `Some("")` (a past-the-guard sentinel that pins the accessor
10275 // doesn't perform a silent `Some("") → None` collapse on the
10276 // empty arm — validate rejects `Some("")` through `EdicaoEmpty`
10277 // but the accessor must ship the raw slot verbatim so a
10278 // validate-time gate regression surfaces at any future edition-
10279 // aware consumer's boundary rather than being silently absorbed
10280 // into the substrate's default edition), `Some("2026")` (the
10281 // canonical 4-digit-ASCII-decimal-year shape every `feira init`
10282 // template scaffolds via [`Caixa::template`] and every
10283 // renderer-side fixture at `caixa-helm/src/lib.rs:978` /
10284 // `caixa-flux/src/lib.rs:2319` / `caixa-mesh/src/lib.rs:3208`
10285 // carries by construction), `Some("2018")` / `Some("2021")` /
10286 // `Some("2024")` (canonical 4-digit-ASCII-decimal-year shapes
10287 // peer with Cargo's `[package] edition` grammar every future-
10288 // introduced sibling to `"2026"` will follow), and eight
10289 // past-the-guard sentinels for the `EdicaoInvalid` refusal cases
10290 // (`Some("2026 ")` trailing-whitespace, `Some(" 2026")` leading-
10291 // whitespace, `Some("2026\n")` embedded-LF, `Some("2026")`
10292 // fullwidth-non-ASCII-lookalike, `Some("v2026")` version-tag-
10293 // prefix, `Some("2026.1")` decimal-shape, `Some("26")` wrong-
10294 // length-numeric, `Some("latest")` free-form-non-year — the
10295 // sentinels pin the accessor doesn't silently absorb the
10296 // refusal cases into a substrate-default-edition fallback).
10297 //
10298 // Fourth and final outer top-level [`Caixa`] `Option<&str>`-
10299 // return scalar accessor pin on the substrate primitive —
10300 // sibling of the peer [`Caixa::licenca`] (6d5bc28),
10301 // [`Caixa::repositorio`] (cc7332d), and [`Caixa::descricao`]
10302 // (3f16e2f) pins that opened the "outer [`Caixa`]
10303 // `Option<&str>` scalar" projection pin pattern this pin folds
10304 // on. Sibling in shape to the peer per-`:placement`
10305 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
10306 // [`crate::aplicacao::Placement::affinity`] (74ec2d3) accessor
10307 // pins on the sibling per-M3-mesh-slot `Option<&str>`-return
10308 // axes, extended onto the outer top-level [`Caixa`] universal-
10309 // axis surface's last unlifted `Option<String>` slot. Pins
10310 // against a future silent detour that returned an owned
10311 // `Option<String>` (which would type-check but silently
10312 // allocate on every accessor call, breaking the zero-cost
10313 // projection every peer sibling accessor carries), a
10314 // `Some("") → None` collapse (which would silently absorb the
10315 // `EdicaoEmpty` refusal case at the accessor boundary and any
10316 // future edition-aware consumer would silently fall back to
10317 // the substrate's default edition on a struct-literal
10318 // `Caixa { edicao: Some(""), .. }`), or a
10319 // `None → Some("2026")` collapse (which would silently reify
10320 // the substrate's default edition at the accessor boundary
10321 // and every downstream consumer keying off the
10322 // `Option::is_none()` discriminator would lose the "author
10323 // omitted the slot" signal).
10324 for edicao in [
10325 None,
10326 Some(""),
10327 Some("2026"),
10328 Some("2018"),
10329 Some("2021"),
10330 Some("2024"),
10331 Some("2026 "),
10332 Some(" 2026"),
10333 Some("2026\n"),
10334 Some("2026"),
10335 Some("v2026"),
10336 Some("2026.1"),
10337 Some("26"),
10338 Some("latest"),
10339 ] {
10340 let c = caixa_with_edicao(edicao);
10341 assert_eq!(
10342 c.edicao(),
10343 edicao,
10344 "Caixa::edicao must return :edicao verbatim (got {:?}, \
10345 expected {edicao:?})",
10346 c.edicao(),
10347 );
10348 assert_eq!(
10349 c.edicao(),
10350 c.edicao.as_deref(),
10351 "Caixa::edicao must byte-equal the raw \
10352 `self.edicao.as_deref()` field access across every \
10353 value in the Option<&str> accept-set",
10354 );
10355 }
10356 }
10357
10358 #[test]
10359 fn validate_edicao_empty_arm_routes_through_accessor() {
10360 // Composition pin: [`Caixa::validate_edicao`]'s empty-arm gate
10361 // must key off [`Caixa::edicao`], not the raw
10362 // `self.edicao.as_deref()` field access. Structurally: a
10363 // `Caixa { edicao: Some(""), .. }` must surface the
10364 // `EdicaoEmpty` refusal exactly, and a
10365 // `Caixa { edicao: Some("2026"), .. }` (the canonical
10366 // 4-digit-ASCII-decimal-year form) must pass validate. The
10367 // pair jointly pins the accessor + validate-gate composition:
10368 // any future silent detour that had the accessor return `None`
10369 // on the empty arm (a `.filter(|s| !s.is_empty())` collapse)
10370 // would silently absorb the `EdicaoEmpty` refusal at the
10371 // accessor boundary and the validate gate would accept a
10372 // struct-literal `Caixa { edicao: Some(""), .. }` — the
10373 // composition pin catches that at caixa-core build time.
10374 //
10375 // Peer of the [`Caixa::licenca`] (6d5bc28)
10376 // `validate_licenca_empty_arm_routes_through_accessor`,
10377 // [`Caixa::repositorio`] (cc7332d)
10378 // `validate_repositorio_empty_arm_routes_through_accessor`,
10379 // and [`Caixa::descricao`] (3f16e2f)
10380 // `validate_descricao_empty_arm_routes_through_accessor`
10381 // composition pins on the sibling outer top-level [`Caixa`]
10382 // `Option<&str>` universal-axis surface — same "the validate /
10383 // shape-gate predicate must route through the substrate-
10384 // primitive typed dispatch" discipline extended onto the
10385 // fourth and final outer top-level [`Caixa`] universal-axis
10386 // `Option<&str>`-composition surface, closing the accessor-
10387 // composition family.
10388 let c = caixa_with_edicao(Some(""));
10389 assert!(
10390 matches!(c.validate_edicao(), Err(ManifestError::EdicaoEmpty)),
10391 "validate_edicao must reject edicao == Some(\"\") with \
10392 EdicaoEmpty — the accessor and the validate gate must \
10393 route through the same substrate-primitive typed dispatch \
10394 on the :edicao empty arm",
10395 );
10396 let c = caixa_with_edicao(Some("2026"));
10397 assert!(
10398 c.validate_edicao().is_ok(),
10399 "validate_edicao must accept edicao == Some(\"2026\") \
10400 (the canonical 4-digit-ASCII-decimal-year shape)",
10401 );
10402 }
10403
10404 #[test]
10405 fn edicao_projects_option_str_by_borrow() {
10406 // The by-borrow pin: [`Caixa::edicao`] returns
10407 // `Option<&str>` by borrow — the `&str` borrows the underlying
10408 // `String` storage of the `Option<String>` slot and the
10409 // accessor must not allocate a fresh `String` on every call.
10410 // Peer of the [`Caixa::licenca`] (6d5bc28),
10411 // [`Caixa::repositorio`] (cc7332d), and [`Caixa::descricao`]
10412 // (3f16e2f) by-borrow pins on the peer outer top-level
10413 // [`Caixa`] `Option<&str>`-return axes, and of the
10414 // per-`:placement`
10415 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) by-
10416 // borrow pin on the peer per-M3-mesh-slot `Option<&str>`-
10417 // return axis, extended onto the fourth and final outer top-
10418 // level [`Caixa`] universal-axis `Option<&str>` shape — the
10419 // accessor's returned `&str` must borrow from `&self` (the
10420 // returned reference's lifetime is tied to `&self`), and
10421 // calling the accessor twice on the same [`Caixa`] must yield
10422 // the same `Option<&str>` verbatim (idempotent, no side
10423 // effects on `&self`).
10424 //
10425 // Pins against a future silent detour that returned an owned
10426 // `Option<String>` (which would type-check but silently
10427 // allocate on every call, breaking the zero-cost projection
10428 // every peer sibling accessor carries), or a one-arm-only
10429 // accessor that returned a saturating value on some sentinel
10430 // input (breaking the pass-through invariant the sibling
10431 // required-scalar accessors carry).
10432 for edicao in [None, Some(""), Some("2026"), Some("2018")] {
10433 let c = caixa_with_edicao(edicao);
10434 let first = c.edicao();
10435 let second = c.edicao();
10436 assert_eq!(
10437 first, second,
10438 "Caixa::edicao must be idempotent — two successive \
10439 calls on the same &self must return the same \
10440 Option<&str>",
10441 );
10442 assert_eq!(
10443 first, edicao,
10444 "Caixa::edicao must return :edicao verbatim by \
10445 borrow — got {first:?}, expected {edicao:?}",
10446 );
10447 }
10448 }
10449
10450 #[test]
10451 fn nome_returns_nome_byte_string_verbatim_across_permutations() {
10452 // The canonical per-`Caixa` `:nome` universal-axis DNS-1123-
10453 // label caixa-identity scalar pin: [`Caixa::nome`] must return
10454 // the `:nome` typed `String` verbatim as `&str`, byte-equal to
10455 // the raw field access across every representative value in
10456 // the accept-set — the canonical `"demo"` template baseline
10457 // (the same `feira init`-scaffolded default the sibling
10458 // `validate_nome_accepts_canonical_template` positive-control
10459 // gate pins), plus every sibling per-typed-slot atom accessor's
10460 // canonical positive-arm byte-string (`"catalog"` per
10461 // [`crate::aplicacao::Membro::nome`], `"cart"` per the peer
10462 // per-`:contratos` `:de`, `"hello-rio"` per the canonical
10463 // `caixa-helm`/`caixa-flux` cross-crate integration-test
10464 // fixture, `"checkout"` per the M3 mesh-slot Aplicacao
10465 // canonical example), plus every past-the-guard sentinel for
10466 // the `NomeEmpty` / `NomeInvalid` / `NomeChartNameBudgetExceeded`
10467 // refusal cases (`""`, `"Bad_Name"`, `"a"` × 56 — 56 bytes fits
10468 // the bare DNS-1123 63-byte cap but overflows the joint
10469 // `lareira-<nome>` chart-name budget the sibling
10470 // [`Caixa::validate_nome_chart_name_budget`] gate closes on).
10471 //
10472 // The past-the-guard sentinels pin the accessor doesn't
10473 // silently absorb the refusal cases into a template-derived
10474 // fallback (a future `.nome().is_empty().then(|| "demo")`
10475 // collapse would silently absorb the `NomeEmpty` refusal at
10476 // the accessor boundary and the validate gate would accept a
10477 // struct-literal `Caixa { nome: "".into(), .. }` — the pin
10478 // catches that at caixa-core build time).
10479 //
10480 // First outer top-level [`Caixa`] `&str`-return required-
10481 // scalar accessor pin — opens the "outer [`Caixa`] `&str`
10482 // required-scalar" projection pattern the sibling per-`Caixa`
10483 // `:versao` future lift folds on. Sibling in shape to the peer
10484 // per-`:membros` [`crate::aplicacao::Membro::nome`] (4a32abf)
10485 // required-`String`-carry accessor pin on the sibling per-
10486 // sub-struct required-axis, extended onto the outer top-level
10487 // [`Caixa`] universal-axis required-`String`-carry axis.
10488 for nome in [
10489 "demo",
10490 "catalog",
10491 "cart",
10492 "hello-rio",
10493 "checkout",
10494 "",
10495 "Bad_Name",
10496 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
10497 ] {
10498 let c = caixa_with_nome(nome);
10499 assert_eq!(
10500 c.nome(),
10501 nome,
10502 "Caixa::nome must return :nome verbatim (got {}, \
10503 expected {nome})",
10504 c.nome(),
10505 );
10506 assert_eq!(
10507 c.nome(),
10508 c.nome.as_str(),
10509 "Caixa::nome must byte-equal the raw .nome field \
10510 access across every value in the String accept-set",
10511 );
10512 }
10513 }
10514
10515 #[test]
10516 fn validate_nome_empty_arm_routes_through_accessor() {
10517 // Composition pin: [`Caixa::validate_nome`]'s empty-arm must
10518 // key off [`Caixa::nome`], not the raw `.nome` field access.
10519 // Structurally: a `Caixa { nome: "".into(), .. }` must surface
10520 // the `NomeEmpty` refusal exactly, and the canonical `"demo"`
10521 // template baseline (the peer positive-arm the sibling
10522 // `validate_nome_accepts_canonical_template` gate carves out)
10523 // must pass validate. The pair jointly pins the accessor +
10524 // validate-gate composition: any future silent detour that
10525 // had the accessor return a fresh `"demo"` on the empty arm
10526 // (a `.nome().is_empty().then(|| "demo")` fallback collapse)
10527 // would silently absorb the `NomeEmpty` refusal at the
10528 // accessor boundary and the validate gate would accept a
10529 // struct-literal `Caixa { nome: "".into(), .. }` — the
10530 // composition pin catches that at caixa-core build time.
10531 //
10532 // Peer of the sibling per-`Caixa`
10533 // `validate_licenca_empty_arm_routes_through_accessor` (6d5bc28)
10534 // / `validate_repositorio_empty_arm_routes_through_accessor`
10535 // (cc7332d) / `validate_descricao_empty_arm_routes_through_accessor`
10536 // (3f16e2f) / `validate_edicao_empty_arm_routes_through_accessor`
10537 // (2641cbd) composition pins on the sibling outer top-level
10538 // [`Caixa`] `Option<&str>` axes — same "the validate /
10539 // shape-gate predicate must route through the substrate-
10540 // primitive typed dispatch" discipline extended onto the peer
10541 // outer top-level [`Caixa`] required-`&str` composition axis.
10542 let c = caixa_with_nome("");
10543 assert!(
10544 matches!(c.validate_nome(), Err(ManifestError::NomeEmpty)),
10545 "validate_nome must reject nome == \"\" with NomeEmpty — \
10546 the accessor and the validate gate must route through the \
10547 same substrate-primitive typed dispatch on the :nome \
10548 empty-arm",
10549 );
10550 let c = caixa_with_nome("demo");
10551 assert!(
10552 c.validate_nome().is_ok(),
10553 "validate_nome must accept nome == \"demo\" (the canonical \
10554 DNS-1123-label template baseline)",
10555 );
10556 }
10557
10558 #[test]
10559 fn nome_projects_str_by_borrow() {
10560 // The by-borrow pin: [`Caixa::nome`] returns `&str` by borrow
10561 // — the `&str` borrows the underlying `String` storage of the
10562 // required `nome` slot and the accessor must not allocate a
10563 // fresh `String` on every call. Peer of the [`Caixa::licenca`]
10564 // (6d5bc28) / [`Caixa::repositorio`] (cc7332d) /
10565 // [`Caixa::descricao`] (3f16e2f) / [`Caixa::edicao`] (2641cbd)
10566 // by-borrow pins on the peer outer top-level [`Caixa`]
10567 // `Option<&str>`-return axes, extended onto the first outer
10568 // top-level [`Caixa`] required-`&str`-return axis — the
10569 // accessor's returned `&str` must borrow from `&self` (the
10570 // returned reference's lifetime is tied to `&self`), and
10571 // calling the accessor twice on the same [`Caixa`] must yield
10572 // the same `&str` verbatim (idempotent, no side effects on
10573 // `&self`).
10574 //
10575 // Pins against a future silent detour that returned an owned
10576 // `String` (which would type-check but silently allocate on
10577 // every call, breaking the zero-cost projection every peer
10578 // sibling accessor carries), an accidental
10579 // `.nome.to_lowercase()` detour that returned a fresh
10580 // allocation through an already-DNS-1123-lowercase-only
10581 // string (breaking a future `const fn` regression), or a
10582 // one-arm-only accessor that returned a canonicalized value
10583 // on some sentinel input (breaking the pass-through invariant
10584 // the sibling required-scalar accessors carry).
10585 for nome in ["demo", "catalog", "hello-rio", "checkout"] {
10586 let c = caixa_with_nome(nome);
10587 let first = c.nome();
10588 let second = c.nome();
10589 assert_eq!(
10590 first, second,
10591 "Caixa::nome must be idempotent — two successive calls \
10592 on the same &self must return the same &str",
10593 );
10594 assert_eq!(
10595 first, nome,
10596 "Caixa::nome must return :nome verbatim by borrow — \
10597 got {first}, expected {nome}",
10598 );
10599 }
10600 }
10601
10602 #[test]
10603 fn versao_returns_versao_byte_string_verbatim_across_permutations() {
10604 // The canonical per-`Caixa` `:versao` universal-axis SemVer-2
10605 // pinned-version scalar pin: [`Caixa::versao`] must return the
10606 // `:versao` typed `String` verbatim as `&str`, byte-equal to the
10607 // raw `.versao` field access across every representative value
10608 // in the accept-set — the canonical `"0.1.0"` template baseline
10609 // (the same `feira init`-scaffolded default the sibling
10610 // `validate_versao_accepts_canonical_template` positive-control
10611 // gate pins), plus every canonical SemVer-2 shape the sibling
10612 // `validate_versao_accepts_canonical_forms` positive-arm sweep
10613 // covers (`"0.0.0"`, `"1.0.0"`, `"0.2.0-rc.1"`,
10614 // `"1.0.0-alpha.0"`, `"1.0.0+build.42"`, `"1.0.0-rc.1+build.42"`,
10615 // `"10.20.30"`), plus every past-the-guard sentinel for the
10616 // `VersaoEmpty` / `VersaoInvalid` refusal cases (`""` the empty
10617 // arm, `"v0.1.0"` the git-tag-shape-leak footgun, `"0.1"` the
10618 // missing-patch footgun, `"^0.1"` the requirement-shape-leak
10619 // footgun, `"0.1.0.0"` the four-part-Java-convention footgun,
10620 // `"latest"` the docker-tag-shape footgun — the sentinels pin
10621 // the accessor doesn't silently absorb the refusal cases into a
10622 // template-derived fallback like `"0.1.0"`).
10623 //
10624 // The past-the-guard sentinels pin the accessor doesn't silently
10625 // absorb the refusal cases into a template-derived fallback (a
10626 // future `.versao().is_empty().then(|| "0.1.0")` collapse would
10627 // silently absorb the `VersaoEmpty` refusal at the accessor
10628 // boundary and the validate gate would accept a struct-literal
10629 // `Caixa { versao: "".into(), .. }` — the pin catches that at
10630 // caixa-core build time).
10631 //
10632 // Second outer top-level [`Caixa`] `&str`-return required-scalar
10633 // accessor pin — folds on the "outer [`Caixa`] `&str` required-
10634 // scalar" projection pattern the sibling per-`Caixa`
10635 // [`Caixa::nome`] (e6b7d97) opened. Sibling in shape to the peer
10636 // per-`:membros` [`crate::aplicacao::Membro::versao_requirement`]
10637 // (4127bb6) / per-`:children`
10638 // [`crate::supervisor::ChildSpec::versao_requirement`] (2c053c8)
10639 // / per-`:upgrade-from`
10640 // [`crate::UpgradeFromEntry::prior_versao`] (75d27a8) per-sub-
10641 // struct `:versao`-shaped `&str`-return accessor pins on the
10642 // sibling per-typed-slot version-carrier axes, extended onto the
10643 // second outer top-level [`Caixa`] universal-axis required-
10644 // `String`-carry axis so the two universal-axis identity-
10645 // carrying scalars every `defcaixa` form supplies (`:nome` +
10646 // `:versao`) share the same "one typed dispatch per axis" pin
10647 // discipline.
10648 for versao in [
10649 "0.1.0",
10650 "0.0.0",
10651 "1.0.0",
10652 "0.2.0-rc.1",
10653 "1.0.0-alpha.0",
10654 "1.0.0+build.42",
10655 "1.0.0-rc.1+build.42",
10656 "10.20.30",
10657 "",
10658 "v0.1.0",
10659 "0.1",
10660 "^0.1",
10661 "0.1.0.0",
10662 "latest",
10663 ] {
10664 let c = caixa_with_versao(versao);
10665 assert_eq!(
10666 c.versao(),
10667 versao,
10668 "Caixa::versao must return :versao verbatim (got {}, \
10669 expected {versao})",
10670 c.versao(),
10671 );
10672 assert_eq!(
10673 c.versao(),
10674 c.versao.as_str(),
10675 "Caixa::versao must byte-equal the raw .versao field \
10676 access across every value in the String accept-set",
10677 );
10678 }
10679 }
10680
10681 #[test]
10682 fn validate_versao_empty_arm_routes_through_accessor() {
10683 // Composition pin: [`Caixa::validate_versao`]'s empty-arm gate
10684 // must key off [`Caixa::versao`], not the raw `.versao` field
10685 // access. Structurally: a `Caixa { versao: "".into(), .. }` must
10686 // surface the `VersaoEmpty` refusal exactly, and the canonical
10687 // `"0.1.0"` template baseline (the peer positive-arm the sibling
10688 // `validate_versao_accepts_canonical_template` gate carves out)
10689 // must pass validate. The pair jointly pins the accessor +
10690 // validate-gate composition: any future silent detour that had
10691 // the accessor return a fresh `"0.1.0"` on the empty arm
10692 // (a `.versao().is_empty().then(|| "0.1.0")` fallback collapse)
10693 // would silently absorb the `VersaoEmpty` refusal at the
10694 // accessor boundary and the validate gate would accept a
10695 // struct-literal `Caixa { versao: "".into(), .. }` — the
10696 // composition pin catches that at caixa-core build time.
10697 //
10698 // Peer of the sibling per-`Caixa`
10699 // `validate_nome_empty_arm_routes_through_accessor` (e6b7d97)
10700 // composition pin on the sibling outer top-level [`Caixa`]
10701 // required-`&str` universal-axis surface — same "the validate /
10702 // shape-gate predicate must route through the substrate-
10703 // primitive typed dispatch" discipline extended onto the peer
10704 // outer top-level [`Caixa`] required-`&str` universal-axis
10705 // pinned-version composition axis, closing the second
10706 // coordinate of the "one canonical typed dispatch per per-Caixa
10707 // required-`&str` universal-axis" discipline.
10708 let c = caixa_with_versao("");
10709 assert!(
10710 matches!(c.validate_versao(), Err(ManifestError::VersaoEmpty)),
10711 "validate_versao must reject versao == \"\" with VersaoEmpty — \
10712 the accessor and the validate gate must route through the \
10713 same substrate-primitive typed dispatch on the :versao \
10714 empty-arm",
10715 );
10716 let c = caixa_with_versao("0.1.0");
10717 assert!(
10718 c.validate_versao().is_ok(),
10719 "validate_versao must accept versao == \"0.1.0\" (the \
10720 canonical SemVer-2 template baseline)",
10721 );
10722 }
10723
10724 #[test]
10725 fn versao_projects_str_by_borrow() {
10726 // The by-borrow pin: [`Caixa::versao`] returns `&str` by borrow
10727 // — the `&str` borrows the underlying `String` storage of the
10728 // required `versao` slot and the accessor must not allocate a
10729 // fresh `String` on every call. Peer of the [`Caixa::nome`]
10730 // (e6b7d97) by-borrow pin on the sibling outer top-level
10731 // [`Caixa`] required-`&str`-return axis, extended onto the
10732 // second outer top-level [`Caixa`] required-`&str`-return
10733 // universal-axis pinned-version surface — the accessor's
10734 // returned `&str` must borrow from `&self` (the returned
10735 // reference's lifetime is tied to `&self`), and calling the
10736 // accessor twice on the same [`Caixa`] must yield the same
10737 // `&str` verbatim (idempotent, no side effects on `&self`).
10738 //
10739 // Pins against a future silent detour that returned an owned
10740 // `String` (which would type-check but silently allocate on
10741 // every call, breaking the zero-cost projection every peer
10742 // sibling accessor carries), an accidental
10743 // `semver::Version::parse(&self.versao).unwrap().to_string()`
10744 // detour that returned a canonicalized fresh allocation through
10745 // an already-canonical byte-string (breaking a future `const fn`
10746 // regression and silently absorbing the `VersaoInvalid` refusal
10747 // at the accessor boundary), or a one-arm-only accessor that
10748 // returned a canonicalized value on some sentinel input
10749 // (breaking the pass-through invariant the sibling required-
10750 // scalar accessors carry).
10751 for versao in ["0.1.0", "1.0.0", "0.2.0-rc.1", "1.0.0+build.42"] {
10752 let c = caixa_with_versao(versao);
10753 let first = c.versao();
10754 let second = c.versao();
10755 assert_eq!(
10756 first, second,
10757 "Caixa::versao must be idempotent — two successive \
10758 calls on the same &self must return the same &str",
10759 );
10760 assert_eq!(
10761 first, versao,
10762 "Caixa::versao must return :versao verbatim by borrow \
10763 — got {first}, expected {versao}",
10764 );
10765 }
10766 }
10767
10768 fn caixa_with_kind(kind: CaixaKind) -> Caixa {
10769 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
10770 c.kind = kind;
10771 c
10772 }
10773
10774 #[test]
10775 fn kind_returns_kind_variant_verbatim_across_permutations() {
10776 // The canonical per-`Caixa` `:kind` universal-axis closed-set-
10777 // enum discriminant pin: [`Caixa::kind`] must return the `:kind`
10778 // typed [`CaixaKind`] variant verbatim by `Copy`, byte-equal to
10779 // the raw `.kind` field access across every variant in the
10780 // closed accept-set (`Biblioteca` — the library kind that
10781 // exports lisp forms; `Binario` — the nix-built executable kind
10782 // under `exe/`; `Servico` — the wasm-component daemon kind
10783 // under `servicos/`; `Supervisor` — the OTP-shaped hierarchical
10784 // reconciliation kind; `Aplicacao` — the M3 typed-mesh
10785 // composition kind).
10786 //
10787 // Pins against a future silent detour that re-derived the kind
10788 // from a peer axis (an accidental fallback to
10789 // `if !servicos.is_empty() { Servico } else if
10790 // !membros.is_empty() { Aplicacao } else { Biblioteca }`
10791 // collapse that read the code-surface / mesh-slot columns into
10792 // the kind discriminator), a variant remap the operator
10793 // authors on one consumer without the other, or a stale-derive
10794 // detour that substituted [`CaixaKind::Biblioteca`] as the
10795 // default when the field held any other variant (which would
10796 // silently collapse the distinction between "author explicitly
10797 // declared `:kind Servico`" and "author declared any other
10798 // kind" every downstream renderer-dispatch site depends on).
10799 //
10800 // First outer top-level [`Caixa`] `Copy`-return required-enum-
10801 // discriminant accessor pin — opens the "outer [`Caixa`]
10802 // `Copy`-return required-discriminant" projection pattern.
10803 // Sibling in shape to the peer per-`:supervisor`
10804 // [`crate::supervisor::SupervisorSpec::estrategia`] (eafb619),
10805 // per-`:placement` [`crate::aplicacao::Placement::estrategia`]
10806 // (921fe1b), and per-`:children`
10807 // [`crate::supervisor::ChildSpec::restart`] (dfb4a81)
10808 // `Copy`-return closed-set-enum discriminant accessor pins on
10809 // the sibling nested-spec typed-slot discriminator axes,
10810 // extended here to the outer top-level [`Caixa`] universal-
10811 // axis surface.
10812 for kind in [
10813 CaixaKind::Biblioteca,
10814 CaixaKind::Binario,
10815 CaixaKind::Servico,
10816 CaixaKind::Supervisor,
10817 CaixaKind::Aplicacao,
10818 ] {
10819 let c = caixa_with_kind(kind);
10820 assert_eq!(
10821 c.kind(),
10822 kind,
10823 "Caixa::kind must return :kind verbatim (got {:?}, \
10824 expected {kind:?})",
10825 c.kind(),
10826 );
10827 assert_eq!(
10828 c.kind(),
10829 c.kind,
10830 "Caixa::kind accessor and .kind field access must \
10831 byte-equal — the accessor is the substrate-primitive \
10832 typed dispatch every downstream kind-gate consumer \
10833 must route through",
10834 );
10835 }
10836 }
10837
10838 #[test]
10839 fn require_kind_reads_through_lifted_kind_accessor() {
10840 // Two-consumer coherence pin: the [`crate::render::require_kind`]
10841 // entry-gate predicate (the canonical two-line
10842 // `require_kind(caixa, Servico)?` prelude every per-Servico /
10843 // per-Aplicacao renderer runs at its entry-point) and the
10844 // sibling [`crate::render::KindMismatch`] error carrier's
10845 // `actual:` field (which names the offending caixa's variant
10846 // in the diagnostic) must both key off the lifted accessor, so
10847 // any future rebrand on the typed slot's reader shape lands at
10848 // exactly one place. Pins the two-site coherence by exercising
10849 // every off-diagonal `(actual, expected)` pair across the
10850 // closed accept-set — the `KindMismatch { actual, expected }`
10851 // surfaced on the mismatch arm must byte-equal the pair the
10852 // accessor returns for each side.
10853 //
10854 // Peer of the sibling per-`:placement`
10855 // `validate_placement_reads_through_lifted_estrategia_accessor`
10856 // (921fe1b) two-arm consumer-coherence pin on the M3 mesh-slot
10857 // `Copy`-return discriminant axis — same "the entry-gate
10858 // predicate and the error carrier's `actual:` field must route
10859 // through the substrate-primitive typed dispatch" discipline
10860 // extended onto the outer top-level [`Caixa`] universal-axis
10861 // discriminant surface.
10862 for expected in [
10863 CaixaKind::Biblioteca,
10864 CaixaKind::Binario,
10865 CaixaKind::Servico,
10866 CaixaKind::Supervisor,
10867 CaixaKind::Aplicacao,
10868 ] {
10869 for actual in [
10870 CaixaKind::Biblioteca,
10871 CaixaKind::Binario,
10872 CaixaKind::Servico,
10873 CaixaKind::Supervisor,
10874 CaixaKind::Aplicacao,
10875 ] {
10876 let c = caixa_with_kind(actual);
10877 let result = crate::render::require_kind(&c, expected);
10878 if expected == actual {
10879 assert!(
10880 result.is_ok(),
10881 "require_kind must accept when actual == expected \
10882 (actual={actual:?}, expected={expected:?})",
10883 );
10884 } else {
10885 let err = result.expect_err("require_kind must reject when actual != expected");
10886 assert_eq!(
10887 err.actual,
10888 c.kind(),
10889 "KindMismatch.actual must byte-equal Caixa::kind() \
10890 — the error carrier's `actual:` field reads \
10891 through the lifted accessor",
10892 );
10893 assert_eq!(
10894 err.expected, expected,
10895 "KindMismatch.expected must byte-equal the \
10896 expected variant passed to require_kind",
10897 );
10898 }
10899 }
10900 }
10901 }
10902
10903 #[test]
10904 fn aplicacao_view_kind_gate_routes_through_accessor() {
10905 // Composition pin: [`Caixa::aplicacao_view`]'s kind-gate arm
10906 // must key off [`Caixa::kind`], not the raw `.kind` field
10907 // access. Structurally: a `Caixa { kind: X, .. }` for any
10908 // non-`Aplicacao` variant must fold to `None` on the
10909 // `aplicacao_view` composer (the "kind mismatch → no typed
10910 // view" contract every downstream Aplicacao consumer keys off
10911 // via `?`), and a `Caixa { kind: Aplicacao, .. }` must fold to
10912 // `Some(_)`. The pair jointly pins the accessor + view-gate
10913 // composition: any future silent detour that had the accessor
10914 // return a fresh [`CaixaKind::Aplicacao`] on some sentinel
10915 // input would silently absorb the kind-mismatch case at the
10916 // accessor boundary and every per-Aplicacao renderer would
10917 // silently render a non-Aplicacao caixa's mesh slots — the
10918 // composition pin catches that at caixa-core build time.
10919 //
10920 // Peer of the sibling per-`Caixa`
10921 // `validate_nome_empty_arm_routes_through_accessor` (e6b7d97) /
10922 // `validate_versao_empty_arm_routes_through_accessor` (20c0539)
10923 // composition pins on the sibling outer top-level [`Caixa`]
10924 // required-`&str` universal-axis surfaces — same "the
10925 // composer / validate gate must route through the substrate-
10926 // primitive typed dispatch" discipline extended onto the
10927 // outer top-level [`Caixa`] `Copy`-return required-
10928 // discriminant composition axis.
10929 for kind in [
10930 CaixaKind::Biblioteca,
10931 CaixaKind::Binario,
10932 CaixaKind::Servico,
10933 CaixaKind::Supervisor,
10934 ] {
10935 let c = caixa_with_kind(kind);
10936 assert!(
10937 c.aplicacao_view().is_none(),
10938 "aplicacao_view must return None on non-Aplicacao \
10939 kind {kind:?} — the composer's kind-gate must route \
10940 through Caixa::kind()",
10941 );
10942 }
10943 let c = caixa_with_kind(CaixaKind::Aplicacao);
10944 assert!(
10945 c.aplicacao_view().is_some(),
10946 "aplicacao_view must return Some on kind Aplicacao — \
10947 the composer's kind-gate must accept the matching arm \
10948 through Caixa::kind()",
10949 );
10950 }
10951
10952 #[test]
10953 fn supervisor_view_kind_gate_routes_through_accessor() {
10954 // Composition pin (mirror of the sibling
10955 // `aplicacao_view_kind_gate_routes_through_accessor` on the
10956 // second `_view` composer): [`Caixa::supervisor_view`]'s kind-
10957 // gate arm must key off [`Caixa::kind`], not the raw `.kind`
10958 // field access. A `Caixa { kind: X, .. }` for any non-
10959 // `Supervisor` variant must fold to `None` on the
10960 // `supervisor_view` composer, and a `Caixa { kind:
10961 // Supervisor, .. }` must fold to `Some(_)`. Same peer
10962 // composition pin discipline on the second `_view` composer
10963 // axis.
10964 for kind in [
10965 CaixaKind::Biblioteca,
10966 CaixaKind::Binario,
10967 CaixaKind::Servico,
10968 CaixaKind::Aplicacao,
10969 ] {
10970 let c = caixa_with_kind(kind);
10971 assert!(
10972 c.supervisor_view().is_none(),
10973 "supervisor_view must return None on non-Supervisor \
10974 kind {kind:?} — the composer's kind-gate must route \
10975 through Caixa::kind()",
10976 );
10977 }
10978 let mut c = caixa_with_kind(CaixaKind::Supervisor);
10979 // A Supervisor caixa needs a strategy + at least one child to
10980 // fold to a Some(_) that also validates; the composer itself
10981 // requires only the kind arm, so bare kind flip is enough to
10982 // pin the `Some(_)` return, but we populate the minimum
10983 // supervisor shape so a future strengthening of the composer
10984 // to reject an empty spec doesn't false-positive this pin.
10985 c.estrategia = Some(crate::supervisor::RestartStrategy::OneForOne);
10986 c.children = vec![crate::supervisor::ChildSpec {
10987 caixa: "child".into(),
10988 versao: "^0.1".into(),
10989 restart: crate::supervisor::RestartPolicy::Permanent,
10990 }];
10991 assert!(
10992 c.supervisor_view().is_some(),
10993 "supervisor_view must return Some on kind Supervisor — \
10994 the composer's kind-gate must accept the matching arm \
10995 through Caixa::kind()",
10996 );
10997 }
10998
10999 #[test]
11000 fn kind_projects_by_copy() {
11001 // The by-`Copy` pin: [`Caixa::kind`] returns a fresh
11002 // [`CaixaKind`] by `Copy` — the accessor must not borrow from
11003 // `&self` (the returned value is owned, `Copy`-projected from
11004 // the underlying [`CaixaKind`] storage; two calls on the same
11005 // [`Caixa`] must yield byte-equal values). Peer of the peer
11006 // per-`:placement` `Placement::estrategia` / per-`:supervisor`
11007 // `SupervisorSpec::estrategia` / per-`:children`
11008 // `ChildSpec::restart` `Copy`-return discriminant accessor
11009 // pins on the sibling nested-spec typed-slot discriminator
11010 // axes, extended onto the first outer top-level [`Caixa`]
11011 // required-`Copy`-return axis — pins against a future silent
11012 // detour that returned `&CaixaKind` (which would type-check
11013 // but silently constrain every consumer's callsite to a
11014 // borrow-shaped dispatch, breaking the zero-cost `Copy`
11015 // projection every peer sibling accessor carries).
11016 for kind in [
11017 CaixaKind::Biblioteca,
11018 CaixaKind::Binario,
11019 CaixaKind::Servico,
11020 CaixaKind::Supervisor,
11021 CaixaKind::Aplicacao,
11022 ] {
11023 let c = caixa_with_kind(kind);
11024 let first: CaixaKind = c.kind();
11025 let second: CaixaKind = c.kind();
11026 assert_eq!(
11027 first, second,
11028 "Caixa::kind must be idempotent — two successive \
11029 calls on the same &self must return the same \
11030 CaixaKind variant",
11031 );
11032 assert_eq!(
11033 first, kind,
11034 "Caixa::kind must return :kind verbatim by Copy — \
11035 got {first:?}, expected {kind:?}",
11036 );
11037 }
11038 }
11039
11040 // ── Caixa::autores — outer top-level &[T] slice accessor ──────────
11041
11042 #[test]
11043 fn autores_returns_autores_slice_verbatim_across_permutations() {
11044 // The canonical per-`Caixa` `:autores` universal-axis maintainer-
11045 // name-list slice pin: [`Caixa::autores`] must return the
11046 // `:autores` typed [`Vec<String>`] list verbatim as a
11047 // `&[String]`, byte-equal to the raw `self.autores.as_slice()`
11048 // access across every representative value in the accept-set —
11049 // `[]` (the "no maintainers declared" arm every existing
11050 // fixture without an `:autores` line carries), `[""]` (a past-
11051 // the-guard sentinel that pins the accessor doesn't perform a
11052 // silent `[""] → []` collapse on the empty-entry arm — validate
11053 // rejects `[""]` through `AutorEmpty` but the accessor must
11054 // ship the raw slot verbatim so a validate-time gate regression
11055 // surfaces at the caixa-helm emit boundary rather than being
11056 // silently absorbed into a maintainer-drop), `["pleme-io"]` (the
11057 // canonical single-maintainer form every `feira init` template
11058 // scaffolds), `["alice", "bob"]` (a canonical multi-maintainer
11059 // form), `["alice <alice@example.com>", "bob <bob@example.com>"]`
11060 // (the canonical RFC-5322 `<name> <email>` form the
11061 // `is_chart_maintainer_name_shape` predicate accepts), and
11062 // `["pleme-io", "pleme-io"]` (a past-the-guard duplicate
11063 // sentinel — validate rejects through `AutorDuplicate` but the
11064 // accessor must ship the raw slot verbatim).
11065 //
11066 // First outer top-level [`Caixa`] `&[T]`-return slice accessor
11067 // pin on the substrate primitive — opens the "outer [`Caixa`]
11068 // `&[T]` slice" projection pattern the sibling per-`Caixa`
11069 // `:etiquetas` / `:deps` / `:deps-dev` / `:exe` / `:bibliotecas`
11070 // / `:servicos` / `:upgrade-from` / `:children` future lifts
11071 // fold on. Sibling in shape to the peer per-`:supervisor`
11072 // [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
11073 // per-`:placement` [`crate::aplicacao::Placement::clusters`]
11074 // (a6e18d7), per-`:membros`
11075 // [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
11076 // per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
11077 // (0dcc926), and per-`:upgrade-from :instructions`
11078 // [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
11079 // `&[T]`-return slice accessor pins on the sibling per-M2 /
11080 // per-M3 typed-slot list axes, extended onto the outer top-
11081 // level [`Caixa`] universal-axis surface. Pins against a future
11082 // silent detour that returned an owned `Vec<String>` (which
11083 // would type-check but silently clone on every accessor call,
11084 // breaking the zero-cost projection every peer sibling slice
11085 // accessor carries), a `[""] → []` collapse (which would
11086 // silently absorb the `AutorEmpty` refusal case at the accessor
11087 // boundary), or a `["a", "a"] → ["a"]` dedup collapse (which
11088 // would silently absorb the `AutorDuplicate` refusal case at
11089 // the accessor boundary and the caixa-helm `maintainers:` fold
11090 // would silently render a dedupped list on a struct-literal
11091 // `Caixa { autores: vec!["a".into(), "a".into()], .. }`).
11092 for autores in [
11093 vec![],
11094 vec![""],
11095 vec!["pleme-io"],
11096 vec!["alice", "bob"],
11097 vec!["alice <alice@example.com>", "bob <bob@example.com>"],
11098 vec!["pleme-io", "pleme-io"],
11099 ] {
11100 let c = caixa_with_autores(autores.clone());
11101 let expected: Vec<String> = autores.iter().map(|s| (*s).to_string()).collect();
11102 assert_eq!(
11103 c.autores(),
11104 expected.as_slice(),
11105 "Caixa::autores must return :autores verbatim (got {:?}, \
11106 expected {expected:?})",
11107 c.autores(),
11108 );
11109 assert_eq!(
11110 c.autores(),
11111 c.autores.as_slice(),
11112 "Caixa::autores must byte-equal the raw \
11113 `self.autores.as_slice()` field access across every \
11114 value in the Vec<String> accept-set",
11115 );
11116 }
11117 }
11118
11119 #[test]
11120 fn validate_autores_empty_entry_arm_routes_through_accessor() {
11121 // Composition pin: [`Caixa::validate_autores`]'s per-entry
11122 // empty-arm gate must key off [`Caixa::autores`], not the raw
11123 // `&self.autores` field-borrow walk. Structurally: a
11124 // `Caixa { autores: vec!["".into()], .. }` must surface the
11125 // `AutorEmpty` refusal exactly, and a
11126 // `Caixa { autores: vec!["pleme-io".into()], .. }` (the
11127 // canonical single-maintainer form) must pass validate. The
11128 // pair jointly pins the accessor + validate-gate composition:
11129 // any future silent detour that had the accessor return an
11130 // empty slice on the `[""]` arm (a
11131 // `.iter().filter(|s| !s.is_empty()).collect()` collapse)
11132 // would silently absorb the `AutorEmpty` refusal at the
11133 // accessor boundary and the validate gate would accept a
11134 // struct-literal `Caixa { autores: vec!["".into()], .. }` —
11135 // the composition pin catches that at caixa-core build time.
11136 //
11137 // Peer of the per-`Caixa` [`Caixa::validate_licenca`] (6d5bc28)
11138 // accessor-composition pin
11139 // (`validate_licenca_empty_arm_routes_through_accessor`) on the
11140 // sibling `Option<&str>`-composition axis and the
11141 // per-`:politicas :circuit-breaker`
11142 // [`crate::aplicacao::CircuitBreaker::max_failures`] (3a74062)
11143 // accessor-composition pin
11144 // (`validate_politicas_max_failures_zero_floor_arm_routes_through_accessor`)
11145 // on the sibling required-`u32`-composition axis — same "the
11146 // validate / shape-gate predicate must route through the
11147 // substrate-primitive typed dispatch" discipline extended onto
11148 // the outer top-level [`Caixa`] universal-axis `&[T]`-
11149 // composition surface.
11150 let c = caixa_with_autores(vec![""]);
11151 assert!(
11152 matches!(c.validate_autores(), Err(ManifestError::AutorEmpty)),
11153 "validate_autores must reject autores == vec![\"\"] with \
11154 AutorEmpty — the accessor and the validate gate must \
11155 route through the same substrate-primitive typed dispatch \
11156 on the :autores per-entry empty arm",
11157 );
11158 let c = caixa_with_autores(vec!["pleme-io"]);
11159 assert!(
11160 c.validate_autores().is_ok(),
11161 "validate_autores must accept autores == vec![\"pleme-io\"] \
11162 (the canonical single-maintainer shape every `feira init` \
11163 template scaffolds)",
11164 );
11165 }
11166
11167 #[test]
11168 fn autores_projects_slice_by_borrow() {
11169 // The by-borrow pin: [`Caixa::autores`] returns `&[String]` by
11170 // borrow — the returned slice borrows the underlying
11171 // `Vec<String>` storage of the `:autores` slot and the
11172 // accessor must not clone the backing `Vec` on every call.
11173 // Peer of the per-`:membros`
11174 // [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36) /
11175 // per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
11176 // (0dcc926) / per-`:placement`
11177 // [`crate::aplicacao::Placement::clusters`] (a6e18d7) /
11178 // per-`:supervisor` [`crate::supervisor::SupervisorSpec::children`]
11179 // (bc92bce) by-borrow pins on the sibling per-M2 / per-M3
11180 // typed-slot `&[T]`-return axes, extended onto the outer top-
11181 // level [`Caixa`] universal-axis `&[String]` shape — the
11182 // accessor's returned slice must borrow from `&self` (the
11183 // returned reference's lifetime is tied to `&self`), and
11184 // calling the accessor twice on the same [`Caixa`] must yield
11185 // slices that are pointer-equal (the underlying byte-buffer is
11186 // the storage `Vec`'s allocation, not a fresh copy) as well as
11187 // value-equal (idempotent, no side effects on `&self`).
11188 //
11189 // Pins against a future silent detour that returned an owned
11190 // `Vec<String>` (which would type-check but silently clone on
11191 // every call, breaking the zero-cost projection every peer
11192 // sibling slice accessor carries), a `&Vec<String>` return
11193 // (which would leak the backing `Vec`'s grow/push/reserve
11194 // surface no downstream consumer reaches for), or a one-arm-
11195 // only accessor that returned a saturating value on some
11196 // sentinel input (breaking the pass-through invariant the
11197 // sibling slice accessors carry).
11198 for autores in [
11199 vec![],
11200 vec!["pleme-io"],
11201 vec!["alice", "bob"],
11202 vec!["pleme-io", "pleme-io"],
11203 ] {
11204 let c = caixa_with_autores(autores.clone());
11205 let expected: Vec<String> = autores.iter().map(|s| (*s).to_string()).collect();
11206 let first = c.autores();
11207 let second = c.autores();
11208 assert_eq!(
11209 first, second,
11210 "Caixa::autores must be idempotent — two successive \
11211 calls on the same &self must return the same \
11212 &[String]",
11213 );
11214 assert_eq!(
11215 first.as_ptr(),
11216 second.as_ptr(),
11217 "Caixa::autores must borrow the underlying Vec<String> \
11218 storage — two successive calls must return slices \
11219 with the same backing pointer (a fresh Vec<String> \
11220 clone would change the pointer on every call)",
11221 );
11222 assert_eq!(
11223 first,
11224 expected.as_slice(),
11225 "Caixa::autores must return :autores verbatim by \
11226 borrow — got {first:?}, expected {expected:?}",
11227 );
11228 }
11229 }
11230
11231 // ── Caixa::etiquetas — outer top-level &[T] slice accessor ────────
11232
11233 #[test]
11234 fn etiquetas_returns_etiquetas_slice_verbatim_across_permutations() {
11235 // The canonical per-`Caixa` `:etiquetas` universal-axis
11236 // registry-search-tag-list slice pin: [`Caixa::etiquetas`] must
11237 // return the `:etiquetas` typed [`Vec<String>`] list verbatim
11238 // as a `&[String]`, byte-equal to the raw
11239 // `self.etiquetas.as_slice()` access across every representative
11240 // value in the accept-set — `[]` (the "no tags declared" arm
11241 // every existing fixture without an `:etiquetas` line carries),
11242 // `[""]` (a past-the-guard sentinel that pins the accessor
11243 // doesn't perform a silent `[""] → []` collapse on the empty-
11244 // entry arm — validate rejects `[""]` through `EtiquetaEmpty`
11245 // but the accessor must ship the raw slot verbatim so a
11246 // validate-time gate regression surfaces at the caixa-helm emit
11247 // boundary rather than being silently absorbed into a keyword-
11248 // drop), `["demo"]` (the canonical single-tag form every
11249 // `feira init` template scaffolds), `["example", "aplicacao",
11250 // "mesh", "ecommerce", "demo"]` (the canonical multi-tag form
11251 // the checkout-aplicacao fixture emits), and `["demo", "demo"]`
11252 // (a past-the-guard duplicate sentinel — validate rejects
11253 // through `EtiquetaDuplicate` but the accessor must ship the
11254 // raw slot verbatim so the caixa-helm `BTreeSet::collect` dedup
11255 // at chart-render time isn't silently promoted into the
11256 // accessor boundary and struct-literal
11257 // `Caixa { etiquetas: vec!["demo".into(), "demo".into()], .. }`
11258 // fixtures continue to expose the duplicate at the accessor).
11259 //
11260 // Second outer top-level [`Caixa`] `&[T]`-return slice accessor
11261 // pin on the substrate primitive — folds on the "outer
11262 // [`Caixa`] `&[T]` slice" projection pattern
11263 // `autores_returns_autores_slice_verbatim_across_permutations`
11264 // (b5d813f) opened, sibling in shape and idiom. Pins against a
11265 // future silent detour that returned an owned `Vec<String>`
11266 // (which would type-check but silently clone on every accessor
11267 // call, breaking the zero-cost projection every peer sibling
11268 // slice accessor carries), a `[""] → []` collapse (which would
11269 // silently absorb the `EtiquetaEmpty` refusal case at the
11270 // accessor boundary), or a `["a", "a"] → ["a"]` dedup collapse
11271 // (which would silently absorb the `EtiquetaDuplicate` refusal
11272 // case at the accessor boundary — the caixa-helm chart-render
11273 // `BTreeSet::collect` dedup is downstream of the accessor and
11274 // must not be silently promoted into it).
11275 for etiquetas in [
11276 vec![],
11277 vec![""],
11278 vec!["demo"],
11279 vec!["example", "aplicacao", "mesh", "ecommerce", "demo"],
11280 vec!["demo", "demo"],
11281 ] {
11282 let c = caixa_with_etiquetas(etiquetas.clone());
11283 let expected: Vec<String> = etiquetas.iter().map(|s| (*s).to_string()).collect();
11284 assert_eq!(
11285 c.etiquetas(),
11286 expected.as_slice(),
11287 "Caixa::etiquetas must return :etiquetas verbatim (got \
11288 {:?}, expected {expected:?})",
11289 c.etiquetas(),
11290 );
11291 assert_eq!(
11292 c.etiquetas(),
11293 c.etiquetas.as_slice(),
11294 "Caixa::etiquetas must byte-equal the raw \
11295 `self.etiquetas.as_slice()` field access across every \
11296 value in the Vec<String> accept-set",
11297 );
11298 }
11299 }
11300
11301 #[test]
11302 fn validate_etiquetas_empty_entry_arm_routes_through_accessor() {
11303 // Composition pin: [`Caixa::validate_etiquetas`]'s per-entry
11304 // empty-arm gate must key off [`Caixa::etiquetas`], not the raw
11305 // `&self.etiquetas` field-borrow walk. Structurally: a
11306 // `Caixa { etiquetas: vec!["".into()], .. }` must surface the
11307 // `EtiquetaEmpty` refusal exactly, and a
11308 // `Caixa { etiquetas: vec!["demo".into()], .. }` (the canonical
11309 // single-tag form) must pass validate. The pair jointly pins
11310 // the accessor + validate-gate composition: any future silent
11311 // detour that had the accessor return an empty slice on the
11312 // `[""]` arm (a
11313 // `.iter().filter(|s| !s.is_empty()).collect()` collapse) would
11314 // silently absorb the `EtiquetaEmpty` refusal at the accessor
11315 // boundary and the validate gate would accept a struct-literal
11316 // `Caixa { etiquetas: vec!["".into()], .. }` — the composition
11317 // pin catches that at caixa-core build time.
11318 //
11319 // Peer of the per-`Caixa` `validate_autores_empty_arm_routes_
11320 // through_accessor` (b5d813f) accessor-composition pin on the
11321 // sibling `&[T]`-composition axis — same "the validate / shape-
11322 // gate predicate must route through the substrate-primitive
11323 // typed dispatch" discipline extended onto the sibling outer
11324 // top-level [`Caixa`] `&[T]`-composition surface.
11325 let c = caixa_with_etiquetas(vec![""]);
11326 assert!(
11327 matches!(c.validate_etiquetas(), Err(ManifestError::EtiquetaEmpty)),
11328 "validate_etiquetas must reject etiquetas == vec![\"\"] \
11329 with EtiquetaEmpty — the accessor and the validate gate \
11330 must route through the same substrate-primitive typed \
11331 dispatch on the :etiquetas per-entry empty arm",
11332 );
11333 let c = caixa_with_etiquetas(vec!["demo"]);
11334 assert!(
11335 c.validate_etiquetas().is_ok(),
11336 "validate_etiquetas must accept etiquetas == vec![\"demo\"] \
11337 (the canonical single-tag shape every `feira init` \
11338 template scaffolds)",
11339 );
11340 }
11341
11342 #[test]
11343 fn etiquetas_projects_slice_by_borrow() {
11344 // The by-borrow pin: [`Caixa::etiquetas`] returns `&[String]`
11345 // by borrow — the returned slice borrows the underlying
11346 // `Vec<String>` storage of the `:etiquetas` slot and the
11347 // accessor must not clone the backing `Vec` on every call.
11348 // Peer of the per-`Caixa` `autores_projects_slice_by_borrow`
11349 // (b5d813f) by-borrow pin on the sibling outer top-level
11350 // [`Caixa`] `&[String]`-return axis — the accessor's returned
11351 // slice must borrow from `&self` (the returned reference's
11352 // lifetime is tied to `&self`), and calling the accessor twice
11353 // on the same [`Caixa`] must yield slices that are pointer-
11354 // equal (the underlying byte-buffer is the storage `Vec`'s
11355 // allocation, not a fresh copy) as well as value-equal
11356 // (idempotent, no side effects on `&self`).
11357 //
11358 // Pins against a future silent detour that returned an owned
11359 // `Vec<String>` (which would type-check but silently clone on
11360 // every call, breaking the zero-cost projection every peer
11361 // sibling slice accessor carries), a `&Vec<String>` return
11362 // (which would leak the backing `Vec`'s grow/push/reserve
11363 // surface no downstream consumer reaches for), or a one-arm-
11364 // only accessor that returned a saturating value on some
11365 // sentinel input (breaking the pass-through invariant the
11366 // sibling slice accessors carry).
11367 for etiquetas in [
11368 vec![],
11369 vec!["demo"],
11370 vec!["example", "aplicacao", "mesh"],
11371 vec!["demo", "demo"],
11372 ] {
11373 let c = caixa_with_etiquetas(etiquetas.clone());
11374 let expected: Vec<String> = etiquetas.iter().map(|s| (*s).to_string()).collect();
11375 let first = c.etiquetas();
11376 let second = c.etiquetas();
11377 assert_eq!(
11378 first, second,
11379 "Caixa::etiquetas must be idempotent — two successive \
11380 calls on the same &self must return the same \
11381 &[String]",
11382 );
11383 assert_eq!(
11384 first.as_ptr(),
11385 second.as_ptr(),
11386 "Caixa::etiquetas must borrow the underlying \
11387 Vec<String> storage — two successive calls must \
11388 return slices with the same backing pointer (a fresh \
11389 Vec<String> clone would change the pointer on every \
11390 call)",
11391 );
11392 assert_eq!(
11393 first,
11394 expected.as_slice(),
11395 "Caixa::etiquetas must return :etiquetas verbatim by \
11396 borrow — got {first:?}, expected {expected:?}",
11397 );
11398 }
11399 }
11400
11401 // ── Caixa::bibliotecas — outer top-level &[T] slice accessor ──────
11402
11403 #[test]
11404 fn bibliotecas_returns_bibliotecas_slice_verbatim_across_permutations() {
11405 // The canonical per-`Caixa` `:bibliotecas` universal-axis
11406 // library-source-path-list slice pin: [`Caixa::bibliotecas`]
11407 // must return the `:bibliotecas` typed [`Vec<String>`] list
11408 // verbatim as a `&[String]`, byte-equal to the raw
11409 // `self.bibliotecas.as_slice()` access across every
11410 // representative value in the accept-set — `[]` (the "no
11411 // libraries declared" arm every `:kind` other than `Biblioteca`
11412 // + every `Biblioteca` relying on the canonical
11413 // `lib/<nome>.lisp` implicit-default path carries; the
11414 // layout's [`crate::LayoutInvariants`] `MissingLib` arm-gate
11415 // fires exactly on this empty-slot + `Biblioteca`-kind
11416 // combination), `[""]` (a past-the-guard sentinel that pins
11417 // the accessor doesn't perform a silent `[""] → []` collapse
11418 // on the empty-entry arm — validate rejects `[""]` through
11419 // `CodePathEmpty { slot: ":bibliotecas" }` but the accessor
11420 // must ship the raw slot verbatim so a validate-time gate
11421 // regression surfaces at the `feira build` phase-1 parse
11422 // boundary rather than being silently absorbed into a
11423 // library-drop), `["lib/demo.lisp"]` (the canonical single-
11424 // entry form `Caixa::template` scaffolds and every `feira init`
11425 // template emits), `["lib/demo.lisp", "lib/helpers.lisp"]`
11426 // (the canonical multi-library form the
11427 // `validate_code_paths_accepts_explicit_relative_paths_on_
11428 // every_slot` fixture emits), and `["lib/foo.lisp",
11429 // "lib/foo.lisp"]` (a past-the-guard duplicate sentinel —
11430 // validate rejects through `CodePathDuplicate { slot:
11431 // ":bibliotecas" }` per the per-slot set-not-multiset gate,
11432 // but the accessor must ship the raw slot verbatim so the
11433 // `feira build` `for entry in caixa.bibliotecas()` parse walk
11434 // sees the duplicate at the accessor boundary and struct-
11435 // literal `Caixa { bibliotecas: vec!["lib/foo.lisp".into(),
11436 // "lib/foo.lisp".into()], .. }` fixtures continue to expose
11437 // the duplicate at the accessor).
11438 //
11439 // Third outer top-level [`Caixa`] `&[T]`-return slice accessor
11440 // pin on the substrate primitive — folds on the "outer
11441 // [`Caixa`] `&[T]` slice" projection pattern
11442 // `autores_returns_autores_slice_verbatim_across_permutations`
11443 // (b5d813f) opened and
11444 // `etiquetas_returns_etiquetas_slice_verbatim_across_permutations`
11445 // (78c7d3c) folded on, sibling in shape and idiom. Pins
11446 // against a future silent detour that returned an owned
11447 // `Vec<String>` (which would type-check but silently clone on
11448 // every accessor call, breaking the zero-cost projection
11449 // every peer sibling slice accessor carries), a `[""] → []`
11450 // collapse (which would silently absorb the `CodePathEmpty`
11451 // refusal case at the accessor boundary), or a `["lib/foo.lisp",
11452 // "lib/foo.lisp"] → ["lib/foo.lisp"]` dedup collapse (which
11453 // would silently absorb the `CodePathDuplicate` refusal case
11454 // at the accessor boundary — the per-slot set-not-multiset
11455 // gate is downstream of the accessor and must not be silently
11456 // promoted into it).
11457 for bibliotecas in [
11458 vec![],
11459 vec![""],
11460 vec!["lib/demo.lisp"],
11461 vec!["lib/demo.lisp", "lib/helpers.lisp"],
11462 vec!["lib/foo.lisp", "lib/foo.lisp"],
11463 ] {
11464 let c = caixa_with_code_paths(bibliotecas.clone(), vec![], vec![]);
11465 let expected: Vec<String> = bibliotecas.iter().map(|s| (*s).to_string()).collect();
11466 assert_eq!(
11467 c.bibliotecas(),
11468 expected.as_slice(),
11469 "Caixa::bibliotecas must return :bibliotecas verbatim \
11470 (got {:?}, expected {expected:?})",
11471 c.bibliotecas(),
11472 );
11473 assert_eq!(
11474 c.bibliotecas(),
11475 c.bibliotecas.as_slice(),
11476 "Caixa::bibliotecas must byte-equal the raw \
11477 `self.bibliotecas.as_slice()` field access across \
11478 every value in the Vec<String> accept-set",
11479 );
11480 }
11481 }
11482
11483 #[test]
11484 fn validate_code_paths_bibliotecas_empty_arm_routes_through_accessor() {
11485 // Composition pin: [`Caixa::validate_code_paths`]'s per-entry
11486 // empty-arm gate on the `:bibliotecas` slot must key off
11487 // [`Caixa::bibliotecas`], not a divergent raw
11488 // `&self.bibliotecas` field-borrow walk. Structurally: a
11489 // `Caixa { bibliotecas: vec!["".into()], .. }` must surface
11490 // the `CodePathEmpty { slot: ":bibliotecas" }` refusal
11491 // exactly, and a `Caixa { bibliotecas: vec!["lib/demo.lisp".
11492 // into()], .. }` (the canonical single-library form
11493 // `Caixa::template` scaffolds) must pass validate. The pair
11494 // jointly pins the accessor + validate-gate composition: any
11495 // future silent detour that had the accessor return an empty
11496 // slice on the `[""]` arm (a `.iter().filter(|s|
11497 // !s.is_empty()).collect()` collapse) would silently absorb
11498 // the `CodePathEmpty` refusal at the accessor boundary and
11499 // the validate gate would accept a struct-literal
11500 // `Caixa { bibliotecas: vec!["".into()], .. }` — the
11501 // composition pin catches that at caixa-core build time.
11502 //
11503 // Peer of the per-`Caixa` `validate_autores_empty_arm_routes_
11504 // through_accessor` (b5d813f) and
11505 // `validate_etiquetas_empty_entry_arm_routes_through_accessor`
11506 // (78c7d3c) accessor-composition pins on the sibling `&[T]`-
11507 // composition axes — same "the validate / shape-gate
11508 // predicate must route through the substrate-primitive typed
11509 // dispatch" discipline extended onto the sibling outer top-
11510 // level [`Caixa`] `&[T]`-composition surface. Nominally the
11511 // in-tree `validate_code_paths` production body still keys
11512 // off the internal `[(":bibliotecas", &self.bibliotecas,
11513 // CodePathFileType::LispSource), (":exe", &self.exe, ..),
11514 // (":servicos", &self.servicos, ..)]` per-slot dispatch tuple
11515 // (the tuple's homogeneous slice-typed shape blocks a per-
11516 // element accessor swap in isolation — a future companion
11517 // lift for `:exe` and `:servicos` on the same outer-`Caixa`
11518 // `&[T]` slice-accessor axis closes that tuple onto the
11519 // triple of typed dispatches as a unit); the composition pin
11520 // catches any future accessor-side silent filter drop against
11521 // that eventual tuple-closure regardless of whether the
11522 // `:bibliotecas` slot is threaded through the accessor or the
11523 // raw field access at the tuple's construction site.
11524 let c = caixa_with_code_paths(vec![""], vec![], vec![]);
11525 assert!(
11526 matches!(
11527 c.validate_code_paths(),
11528 Err(ManifestError::CodePathEmpty {
11529 slot: ":bibliotecas"
11530 })
11531 ),
11532 "validate_code_paths must reject bibliotecas == vec![\"\"] \
11533 with CodePathEmpty {{ slot: \":bibliotecas\" }} — the \
11534 accessor and the validate gate must route through the \
11535 same substrate-primitive typed dispatch on the \
11536 :bibliotecas per-entry empty arm",
11537 );
11538 let c = caixa_with_code_paths(vec!["lib/demo.lisp"], vec![], vec![]);
11539 assert!(
11540 c.validate_code_paths().is_ok(),
11541 "validate_code_paths must accept bibliotecas == \
11542 vec![\"lib/demo.lisp\"] (the canonical single-library \
11543 shape every `feira init` template scaffolds)",
11544 );
11545 }
11546
11547 #[test]
11548 fn bibliotecas_projects_slice_by_borrow() {
11549 // The by-borrow pin: [`Caixa::bibliotecas`] returns
11550 // `&[String]` by borrow — the returned slice borrows the
11551 // underlying `Vec<String>` storage of the `:bibliotecas` slot
11552 // and the accessor must not clone the backing `Vec` on every
11553 // call. Peer of the per-`Caixa` `autores_projects_slice_by_borrow`
11554 // (b5d813f) and `etiquetas_projects_slice_by_borrow` (78c7d3c)
11555 // by-borrow pins on the sibling outer top-level [`Caixa`]
11556 // `&[String]`-return axes — the accessor's returned slice
11557 // must borrow from `&self` (the returned reference's lifetime
11558 // is tied to `&self`), and calling the accessor twice on the
11559 // same [`Caixa`] must yield slices that are pointer-equal
11560 // (the underlying byte-buffer is the storage `Vec`'s
11561 // allocation, not a fresh copy) as well as value-equal
11562 // (idempotent, no side effects on `&self`).
11563 //
11564 // Pins against a future silent detour that returned an owned
11565 // `Vec<String>` (which would type-check but silently clone on
11566 // every call, breaking the zero-cost projection every peer
11567 // sibling slice accessor carries), a `&Vec<String>` return
11568 // (which would leak the backing `Vec`'s grow/push/reserve
11569 // surface no downstream consumer reaches for), or a one-arm-
11570 // only accessor that returned a saturating value on some
11571 // sentinel input (breaking the pass-through invariant the
11572 // sibling slice accessors carry).
11573 for bibliotecas in [
11574 vec![],
11575 vec!["lib/demo.lisp"],
11576 vec!["lib/demo.lisp", "lib/helpers.lisp"],
11577 vec!["lib/foo.lisp", "lib/foo.lisp"],
11578 ] {
11579 let c = caixa_with_code_paths(bibliotecas.clone(), vec![], vec![]);
11580 let expected: Vec<String> = bibliotecas.iter().map(|s| (*s).to_string()).collect();
11581 let first = c.bibliotecas();
11582 let second = c.bibliotecas();
11583 assert_eq!(
11584 first, second,
11585 "Caixa::bibliotecas must be idempotent — two \
11586 successive calls on the same &self must return the \
11587 same &[String]",
11588 );
11589 assert_eq!(
11590 first.as_ptr(),
11591 second.as_ptr(),
11592 "Caixa::bibliotecas must borrow the underlying \
11593 Vec<String> storage — two successive calls must \
11594 return slices with the same backing pointer (a \
11595 fresh Vec<String> clone would change the pointer on \
11596 every call)",
11597 );
11598 assert_eq!(
11599 first,
11600 expected.as_slice(),
11601 "Caixa::bibliotecas must return :bibliotecas verbatim \
11602 by borrow — got {first:?}, expected {expected:?}",
11603 );
11604 }
11605 }
11606
11607 // ── Caixa::exe — outer top-level &[T] slice accessor ──────────────
11608
11609 #[test]
11610 fn exe_returns_exe_slice_verbatim_across_permutations() {
11611 // The canonical per-`Caixa` `:exe` universal-axis
11612 // nix-built-executable-entry-path-list slice pin: [`Caixa::exe`]
11613 // must return the `:exe` typed [`Vec<String>`] list verbatim as
11614 // a `&[String]`, byte-equal to the raw `self.exe.as_slice()`
11615 // access across every representative value in the accept-set —
11616 // `[]` (the "no executable declared" arm every `:kind` other
11617 // than `Binario` carries; the layout's [`crate::LayoutInvariants`]
11618 // `BinarioWithoutExe` arm-gate fires exactly on this empty-slot
11619 // + `Binario`-kind combination), `[""]` (a past-the-guard
11620 // sentinel that pins the accessor doesn't perform a silent
11621 // `[""] → []` collapse on the empty-entry arm — validate rejects
11622 // `[""]` through `CodePathEmpty { slot: ":exe" }` but the
11623 // accessor must ship the raw slot verbatim so a validate-time
11624 // gate regression surfaces at the layout / `feira nix` boundary
11625 // rather than being silently absorbed into an executable-drop),
11626 // `["exe/cli"]` (the canonical single-entry Binario form every
11627 // in-tree `caixa_with_code_paths` positive control uses),
11628 // `["exe/cli", "exe/serve"]` (the canonical multi-executable
11629 // form the `validate_code_paths_accepts_explicit_relative_paths_
11630 // on_every_slot` fixture emits), and `["exe/cli", "exe/cli"]`
11631 // (a past-the-guard duplicate sentinel — validate rejects
11632 // through `CodePathDuplicate { slot: ":exe" }` per the per-slot
11633 // set-not-multiset gate, but the accessor must ship the raw
11634 // slot verbatim so struct-literal `Caixa { exe: vec!["exe/cli".
11635 // into(), "exe/cli".into()], .. }` fixtures continue to expose
11636 // the duplicate at the accessor).
11637 //
11638 // Fourth outer top-level [`Caixa`] `&[T]`-return slice accessor
11639 // pin on the substrate primitive — folds on the "outer
11640 // [`Caixa`] `&[T]` slice" projection pattern
11641 // `autores_returns_autores_slice_verbatim_across_permutations`
11642 // (b5d813f) opened,
11643 // `etiquetas_returns_etiquetas_slice_verbatim_across_permutations`
11644 // (78c7d3c) folded on, and
11645 // `bibliotecas_returns_bibliotecas_slice_verbatim_across_permutations`
11646 // (8a36c23) closed the universal-axis text-tag family of.
11647 // Opens the outer-`Caixa` foreign-code-slot `&[T]` sub-family
11648 // the sibling `:servicos` future lift closes onto. Pins against
11649 // a future silent detour that returned an owned `Vec<String>`
11650 // (which would type-check but silently clone on every accessor
11651 // call, breaking the zero-cost projection every peer sibling
11652 // slice accessor carries), a `[""] → []` collapse (which would
11653 // silently absorb the `CodePathEmpty` refusal case at the
11654 // accessor boundary), or an `["exe/cli", "exe/cli"] →
11655 // ["exe/cli"]` dedup collapse (which would silently absorb the
11656 // `CodePathDuplicate` refusal case at the accessor boundary —
11657 // the per-slot set-not-multiset gate is downstream of the
11658 // accessor and must not be silently promoted into it).
11659 for exe in [
11660 vec![],
11661 vec![""],
11662 vec!["exe/cli"],
11663 vec!["exe/cli", "exe/serve"],
11664 vec!["exe/cli", "exe/cli"],
11665 ] {
11666 let c = caixa_with_code_paths(vec![], exe.clone(), vec![]);
11667 let expected: Vec<String> = exe.iter().map(|s| (*s).to_string()).collect();
11668 assert_eq!(
11669 c.exe(),
11670 expected.as_slice(),
11671 "Caixa::exe must return :exe verbatim (got {:?}, \
11672 expected {expected:?})",
11673 c.exe(),
11674 );
11675 assert_eq!(
11676 c.exe(),
11677 c.exe.as_slice(),
11678 "Caixa::exe must byte-equal the raw \
11679 `self.exe.as_slice()` field access across every value \
11680 in the Vec<String> accept-set",
11681 );
11682 }
11683 }
11684
11685 #[test]
11686 fn validate_code_paths_exe_empty_arm_routes_through_accessor() {
11687 // Composition pin: [`Caixa::validate_code_paths`]'s per-entry
11688 // empty-arm gate on the `:exe` slot must key off
11689 // [`Caixa::exe`], not a divergent raw `&self.exe` field-borrow
11690 // walk. Structurally: a `Caixa { exe: vec!["".into()], .. }`
11691 // must surface the `CodePathEmpty { slot: ":exe" }` refusal
11692 // exactly, and a `Caixa { exe: vec!["exe/cli".into()], .. }`
11693 // (the canonical single-executable form every in-tree
11694 // `caixa_with_code_paths` positive control uses) must pass
11695 // validate. The pair jointly pins the accessor + validate-gate
11696 // composition: any future silent detour that had the accessor
11697 // return an empty slice on the `[""]` arm (a
11698 // `.iter().filter(|s| !s.is_empty()).collect()` collapse) would
11699 // silently absorb the `CodePathEmpty` refusal at the accessor
11700 // boundary and the validate gate would accept a struct-literal
11701 // `Caixa { exe: vec!["".into()], .. }` — the composition pin
11702 // catches that at caixa-core build time.
11703 //
11704 // Peer of the per-`Caixa`
11705 // `validate_code_paths_bibliotecas_empty_arm_routes_through_accessor`
11706 // (8a36c23), `validate_autores_empty_arm_routes_through_accessor`
11707 // (b5d813f), and
11708 // `validate_etiquetas_empty_entry_arm_routes_through_accessor`
11709 // (78c7d3c) accessor-composition pins on the sibling `&[T]`-
11710 // composition axes — same "the validate / shape-gate predicate
11711 // must route through the substrate-primitive typed dispatch"
11712 // discipline extended onto the sibling outer top-level [`Caixa`]
11713 // `&[T]`-composition surface. Nominally the in-tree
11714 // `validate_code_paths` production body still keys off the
11715 // internal `[(":bibliotecas", &self.bibliotecas,
11716 // CodePathFileType::LispSource), (":exe", &self.exe, ..),
11717 // (":servicos", &self.servicos, ..)]` per-slot dispatch tuple
11718 // (the tuple's homogeneous slice-typed shape blocks a per-
11719 // element accessor swap in isolation — a future companion lift
11720 // for `:servicos` on the same outer-`Caixa` `&[T]` slice-
11721 // accessor axis closes that tuple onto the triple of typed
11722 // dispatches as a unit); the composition pin catches any future
11723 // accessor-side silent filter drop against that eventual tuple-
11724 // closure regardless of whether the `:exe` slot is threaded
11725 // through the accessor or the raw field access at the tuple's
11726 // construction site.
11727 let c = caixa_with_code_paths(vec![], vec![""], vec![]);
11728 assert!(
11729 matches!(
11730 c.validate_code_paths(),
11731 Err(ManifestError::CodePathEmpty { slot: ":exe" })
11732 ),
11733 "validate_code_paths must reject exe == vec![\"\"] \
11734 with CodePathEmpty {{ slot: \":exe\" }} — the \
11735 accessor and the validate gate must route through the \
11736 same substrate-primitive typed dispatch on the \
11737 :exe per-entry empty arm",
11738 );
11739 let c = caixa_with_code_paths(vec![], vec!["exe/cli"], vec![]);
11740 assert!(
11741 c.validate_code_paths().is_ok(),
11742 "validate_code_paths must accept exe == vec![\"exe/cli\"] \
11743 (the canonical single-executable shape every in-tree \
11744 `caixa_with_code_paths` positive control uses)",
11745 );
11746 }
11747
11748 #[test]
11749 fn exe_projects_slice_by_borrow() {
11750 // The by-borrow pin: [`Caixa::exe`] returns `&[String]` by
11751 // borrow — the returned slice borrows the underlying
11752 // `Vec<String>` storage of the `:exe` slot and the accessor
11753 // must not clone the backing `Vec` on every call. Peer of the
11754 // per-`Caixa` `autores_projects_slice_by_borrow` (b5d813f),
11755 // `etiquetas_projects_slice_by_borrow` (78c7d3c), and
11756 // `bibliotecas_projects_slice_by_borrow` (8a36c23) by-borrow
11757 // pins on the sibling outer top-level [`Caixa`] `&[String]`-
11758 // return axes — the accessor's returned slice must borrow from
11759 // `&self` (the returned reference's lifetime is tied to
11760 // `&self`), and calling the accessor twice on the same
11761 // [`Caixa`] must yield slices that are pointer-equal (the
11762 // underlying byte-buffer is the storage `Vec`'s allocation,
11763 // not a fresh copy) as well as value-equal (idempotent, no
11764 // side effects on `&self`).
11765 //
11766 // Pins against a future silent detour that returned an owned
11767 // `Vec<String>` (which would type-check but silently clone on
11768 // every call, breaking the zero-cost projection every peer
11769 // sibling slice accessor carries), a `&Vec<String>` return
11770 // (which would leak the backing `Vec`'s grow/push/reserve
11771 // surface no downstream consumer reaches for), or a one-arm-
11772 // only accessor that returned a saturating value on some
11773 // sentinel input (breaking the pass-through invariant the
11774 // sibling slice accessors carry).
11775 for exe in [
11776 vec![],
11777 vec!["exe/cli"],
11778 vec!["exe/cli", "exe/serve"],
11779 vec!["exe/cli", "exe/cli"],
11780 ] {
11781 let c = caixa_with_code_paths(vec![], exe.clone(), vec![]);
11782 let expected: Vec<String> = exe.iter().map(|s| (*s).to_string()).collect();
11783 let first = c.exe();
11784 let second = c.exe();
11785 assert_eq!(
11786 first, second,
11787 "Caixa::exe must be idempotent — two successive calls \
11788 on the same &self must return the same &[String]",
11789 );
11790 assert_eq!(
11791 first.as_ptr(),
11792 second.as_ptr(),
11793 "Caixa::exe must borrow the underlying Vec<String> \
11794 storage — two successive calls must return slices \
11795 with the same backing pointer (a fresh Vec<String> \
11796 clone would change the pointer on every call)",
11797 );
11798 assert_eq!(
11799 first,
11800 expected.as_slice(),
11801 "Caixa::exe must return :exe verbatim by borrow — \
11802 got {first:?}, expected {expected:?}",
11803 );
11804 }
11805 }
11806
11807 // ── Caixa::servicos — outer top-level &[T] slice accessor ─────────
11808
11809 #[test]
11810 fn servicos_returns_servicos_slice_verbatim_across_permutations() {
11811 // The canonical per-`Caixa` `:servicos` universal-axis
11812 // ComputeUnit-CR-YAML-entry-path-list slice pin:
11813 // [`Caixa::servicos`] must return the `:servicos` typed
11814 // [`Vec<String>`] list verbatim as a `&[String]`, byte-equal to
11815 // the raw `self.servicos.as_slice()` access across every
11816 // representative value in the accept-set — `[]` (the "no
11817 // ComputeUnit-CR declared" arm every `:kind` other than
11818 // `Servico` carries; the layout's [`crate::LayoutInvariants`]
11819 // `ServicoWithoutServicos` arm-gate fires exactly on this
11820 // empty-slot + `Servico`-kind combination), `[""]` (a past-the-
11821 // guard sentinel that pins the accessor doesn't perform a
11822 // silent `[""] → []` collapse on the empty-entry arm — validate
11823 // rejects `[""]` through `CodePathEmpty { slot: ":servicos" }`
11824 // but the accessor must ship the raw slot verbatim so a
11825 // validate-time gate regression surfaces at the layout /
11826 // per-Servico renderer boundary rather than being silently
11827 // absorbed into a component-drop),
11828 // `["servicos/demo.computeunit.yaml"]` (the canonical
11829 // singleton V0-shape every in-tree `caixa_with_code_paths`
11830 // positive control uses; the same shape
11831 // [`crate::require_single_servico`] admits),
11832 // `["servicos/a.computeunit.yaml", "servicos/b.computeunit.
11833 // yaml"]` (a past-the-guard `len != 1` sentinel — the V0
11834 // singularity gate rejects through `ServicoCountMismatch
11835 // { count: 2 }` but the accessor must ship the raw slot
11836 // verbatim so struct-literal `Caixa { servicos: vec![...,
11837 // ...], .. }` fixtures continue to expose the count at the
11838 // accessor), and `["servicos/a.computeunit.yaml",
11839 // "servicos/a.computeunit.yaml"]` (a past-the-guard duplicate
11840 // sentinel — validate rejects through
11841 // `CodePathDuplicate { slot: ":servicos" }` per the per-slot
11842 // set-not-multiset gate, but the accessor must ship the raw
11843 // slot verbatim so struct-literal fixtures continue to expose
11844 // the duplicate at the accessor).
11845 //
11846 // Fifth and final outer top-level [`Caixa`] `&[T]`-return
11847 // slice accessor pin on the substrate primitive — folds on the
11848 // "outer [`Caixa`] `&[T]` slice" projection pattern
11849 // `autores_returns_autores_slice_verbatim_across_permutations`
11850 // (b5d813f) opened,
11851 // `etiquetas_returns_etiquetas_slice_verbatim_across_permutations`
11852 // (78c7d3c) folded on,
11853 // `bibliotecas_returns_bibliotecas_slice_verbatim_across_permutations`
11854 // (8a36c23) closed the universal-axis text-tag family of, and
11855 // `exe_returns_exe_slice_verbatim_across_permutations`
11856 // (65d9527) opened the foreign-code-slot sub-family of. Closes
11857 // the outer-`Caixa` foreign-code-slot `&[T]` sub-family — the
11858 // trio of code-surface list slots (`:bibliotecas` + `:exe` +
11859 // `:servicos`) now each carries a substrate-canonical slice
11860 // accessor. Pins against a future silent detour that returned
11861 // an owned `Vec<String>` (which would type-check but silently
11862 // clone on every accessor call, breaking the zero-cost
11863 // projection every peer sibling slice accessor carries), a
11864 // `[""] → []` collapse (which would silently absorb the
11865 // `CodePathEmpty` refusal case at the accessor boundary), an
11866 // `[a, a] → [a]` dedup collapse (which would silently absorb
11867 // the `CodePathDuplicate` refusal case at the accessor
11868 // boundary — the per-slot set-not-multiset gate is downstream
11869 // of the accessor and must not be silently promoted into it),
11870 // or a `[a, b] → [a]` singleton collapse (which would silently
11871 // absorb the V0 `ServicoCountMismatch` refusal case at the
11872 // accessor boundary — the V0 singularity gate is downstream of
11873 // the accessor and must not be silently promoted into it).
11874 for servicos in [
11875 vec![],
11876 vec![""],
11877 vec!["servicos/demo.computeunit.yaml"],
11878 vec!["servicos/a.computeunit.yaml", "servicos/b.computeunit.yaml"],
11879 vec!["servicos/a.computeunit.yaml", "servicos/a.computeunit.yaml"],
11880 ] {
11881 let c = caixa_with_code_paths(vec![], vec![], servicos.clone());
11882 let expected: Vec<String> = servicos.iter().map(|s| (*s).to_string()).collect();
11883 assert_eq!(
11884 c.servicos(),
11885 expected.as_slice(),
11886 "Caixa::servicos must return :servicos verbatim (got \
11887 {:?}, expected {expected:?})",
11888 c.servicos(),
11889 );
11890 assert_eq!(
11891 c.servicos(),
11892 c.servicos.as_slice(),
11893 "Caixa::servicos must byte-equal the raw \
11894 `self.servicos.as_slice()` field access across every \
11895 value in the Vec<String> accept-set",
11896 );
11897 }
11898 }
11899
11900 #[test]
11901 fn validate_code_paths_servicos_empty_arm_routes_through_accessor() {
11902 // Composition pin: [`Caixa::validate_code_paths`]'s per-entry
11903 // empty-arm gate on the `:servicos` slot must key off
11904 // [`Caixa::servicos`], not a divergent raw `&self.servicos`
11905 // field-borrow walk. Structurally: a `Caixa { servicos:
11906 // vec!["".into()], .. }` must surface the `CodePathEmpty
11907 // { slot: ":servicos" }` refusal exactly, and a `Caixa
11908 // { servicos: vec!["servicos/demo.computeunit.yaml".into()],
11909 // .. }` (the canonical singleton V0-shape every in-tree
11910 // `caixa_with_code_paths` positive control uses) must pass
11911 // validate. The pair jointly pins the accessor + validate-gate
11912 // composition: any future silent detour that had the accessor
11913 // return an empty slice on the `[""]` arm (a `.iter().filter
11914 // (|s| !s.is_empty()).collect()` collapse) would silently
11915 // absorb the `CodePathEmpty` refusal at the accessor boundary
11916 // and the validate gate would accept a struct-literal
11917 // `Caixa { servicos: vec!["".into()], .. }` — the composition
11918 // pin catches that at caixa-core build time.
11919 //
11920 // Peer of the per-`Caixa`
11921 // `validate_code_paths_bibliotecas_empty_arm_routes_through_accessor`
11922 // (8a36c23), `validate_code_paths_exe_empty_arm_routes_through_accessor`
11923 // (65d9527), `validate_autores_empty_arm_routes_through_accessor`
11924 // (b5d813f), and
11925 // `validate_etiquetas_empty_entry_arm_routes_through_accessor`
11926 // (78c7d3c) accessor-composition pins on the sibling `&[T]`-
11927 // composition axes — same "the validate / shape-gate predicate
11928 // must route through the substrate-primitive typed dispatch"
11929 // discipline extended onto the sibling outer top-level
11930 // [`Caixa`] `&[T]`-composition surface, closing the trio of
11931 // code-surface accessor-composition pins on the same axis.
11932 // Nominally the in-tree `validate_code_paths` production body
11933 // still keys off the internal
11934 // `[(":bibliotecas", &self.bibliotecas,
11935 // CodePathFileType::LispSource), (":exe", &self.exe, ..),
11936 // (":servicos", &self.servicos, ..)]` per-slot dispatch tuple
11937 // (the tuple's homogeneous `&Vec<String>`-typed shape blocks a
11938 // per-element accessor swap in isolation — a future companion
11939 // lift promotes the tuple's element type to `&[String]` and
11940 // threads the triple of typed dispatches through as a unit);
11941 // the composition pin catches any future accessor-side silent
11942 // filter drop against that eventual tuple-closure regardless
11943 // of whether the `:servicos` slot is threaded through the
11944 // accessor or the raw field access at the tuple's construction
11945 // site.
11946 let c = caixa_with_code_paths(vec![], vec![], vec![""]);
11947 assert!(
11948 matches!(
11949 c.validate_code_paths(),
11950 Err(ManifestError::CodePathEmpty { slot: ":servicos" })
11951 ),
11952 "validate_code_paths must reject servicos == vec![\"\"] \
11953 with CodePathEmpty {{ slot: \":servicos\" }} — the \
11954 accessor and the validate gate must route through the \
11955 same substrate-primitive typed dispatch on the \
11956 :servicos per-entry empty arm",
11957 );
11958 let c = caixa_with_code_paths(vec![], vec![], vec!["servicos/demo.computeunit.yaml"]);
11959 assert!(
11960 c.validate_code_paths().is_ok(),
11961 "validate_code_paths must accept servicos == \
11962 vec![\"servicos/demo.computeunit.yaml\"] (the canonical \
11963 singleton V0-shape every in-tree `caixa_with_code_paths` \
11964 positive control uses)",
11965 );
11966 }
11967
11968 #[test]
11969 fn servicos_projects_slice_by_borrow() {
11970 // The by-borrow pin: [`Caixa::servicos`] returns `&[String]` by
11971 // borrow — the returned slice borrows the underlying
11972 // `Vec<String>` storage of the `:servicos` slot and the
11973 // accessor must not clone the backing `Vec` on every call.
11974 // Peer of the per-`Caixa` `autores_projects_slice_by_borrow`
11975 // (b5d813f), `etiquetas_projects_slice_by_borrow` (78c7d3c),
11976 // `bibliotecas_projects_slice_by_borrow` (8a36c23), and
11977 // `exe_projects_slice_by_borrow` (65d9527) by-borrow pins on
11978 // the sibling outer top-level [`Caixa`] `&[String]`-return
11979 // axes — the accessor's returned slice must borrow from
11980 // `&self` (the returned reference's lifetime is tied to
11981 // `&self`), and calling the accessor twice on the same
11982 // [`Caixa`] must yield slices that are pointer-equal (the
11983 // underlying byte-buffer is the storage `Vec`'s allocation,
11984 // not a fresh copy) as well as value-equal (idempotent, no
11985 // side effects on `&self`).
11986 //
11987 // Pins against a future silent detour that returned an owned
11988 // `Vec<String>` (which would type-check but silently clone on
11989 // every call, breaking the zero-cost projection every peer
11990 // sibling slice accessor carries), a `&Vec<String>` return
11991 // (which would leak the backing `Vec`'s grow/push/reserve
11992 // surface no downstream consumer reaches for), or a one-arm-
11993 // only accessor that returned a saturating value on some
11994 // sentinel input (breaking the pass-through invariant the
11995 // sibling slice accessors carry).
11996 for servicos in [
11997 vec![],
11998 vec!["servicos/demo.computeunit.yaml"],
11999 vec!["servicos/a.computeunit.yaml", "servicos/b.computeunit.yaml"],
12000 vec!["servicos/a.computeunit.yaml", "servicos/a.computeunit.yaml"],
12001 ] {
12002 let c = caixa_with_code_paths(vec![], vec![], servicos.clone());
12003 let expected: Vec<String> = servicos.iter().map(|s| (*s).to_string()).collect();
12004 let first = c.servicos();
12005 let second = c.servicos();
12006 assert_eq!(
12007 first, second,
12008 "Caixa::servicos must be idempotent — two successive \
12009 calls on the same &self must return the same &[String]",
12010 );
12011 assert_eq!(
12012 first.as_ptr(),
12013 second.as_ptr(),
12014 "Caixa::servicos must borrow the underlying \
12015 Vec<String> storage — two successive calls must \
12016 return slices with the same backing pointer (a fresh \
12017 Vec<String> clone would change the pointer on every \
12018 call)",
12019 );
12020 assert_eq!(
12021 first,
12022 expected.as_slice(),
12023 "Caixa::servicos must return :servicos verbatim by \
12024 borrow — got {first:?}, expected {expected:?}",
12025 );
12026 }
12027 }
12028
12029 // ── Caixa::deps — outer top-level &[Dep] slice accessor ───────────
12030
12031 fn caixa_with_deps(deps: Vec<Dep>) -> Caixa {
12032 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
12033 c.deps = deps;
12034 c
12035 }
12036
12037 #[test]
12038 fn deps_returns_deps_slice_verbatim_across_permutations() {
12039 // The canonical per-`Caixa` `:deps` universal-axis runtime-
12040 // dependency-declaration-list slice pin: [`Caixa::deps`] must
12041 // return the `:deps` typed [`Vec<Dep>`] list verbatim as a
12042 // `&[Dep]`, element-equal to the raw `self.deps.as_slice()`
12043 // access across every representative value in the accept-set —
12044 // `[]` (the "no runtime deps declared" arm every existing
12045 // fixture without a `:deps` line carries; the
12046 // [`Caixa::template`] scaffold emits `:deps ()`), a canonical
12047 // single-entry list (the shape most consumer caixas carry), a
12048 // canonical two-entry list (the multi-dep runtime closure), and
12049 // two past-the-guard sentinels — a `[""]`-`:nome` entry
12050 // ([`Self::validate_deps`] rejects through `NomeEmpty` /
12051 // `NomeInvalid` but the accessor must ship the raw slot
12052 // verbatim) and a `[a, a]` duplicate (validate rejects through
12053 // `DuplicateNome { list: ":deps" }` but the accessor must ship
12054 // the raw slot verbatim so struct-literal fixtures continue to
12055 // expose the duplicate at the accessor).
12056 //
12057 // First outer top-level [`Caixa`] `&[Dep]`-return slice accessor
12058 // pin on the substrate primitive — opens the outer-`Caixa`
12059 // dependency-slot `&[Dep]` sub-family the sibling `:deps-dev`
12060 // future lift closes on. Peer of the closed outer-`Caixa`
12061 // foreign-code-slot `&[String]` sub-family
12062 // (`bibliotecas_returns_bibliotecas_slice_verbatim_across_permutations`
12063 // 8a36c23, `exe_returns_exe_slice_verbatim_across_permutations`
12064 // 65d9527, `servicos_returns_servicos_slice_verbatim_across_permutations`
12065 // 611f78b) and the outer-`Caixa` universal-axis text-tag family
12066 // (`autores_returns_autores_slice_verbatim_across_permutations`
12067 // b5d813f, `etiquetas_returns_etiquetas_slice_verbatim_across_permutations`
12068 // 78c7d3c) — extends the "outer [`Caixa`] `&[T]` slice"
12069 // projection pattern onto a novel element-type axis (`Dep`
12070 // composite vs the prior sibling family's `String` scalar).
12071 // Pins against a future silent detour that returned an owned
12072 // `Vec<Dep>` (which would type-check but silently clone on every
12073 // accessor call, breaking the zero-cost projection every peer
12074 // sibling slice accessor carries), a `[""] → []` collapse (which
12075 // would silently absorb the `NomeEmpty` refusal case at the
12076 // accessor boundary), or a `[a, a] → [a]` dedup collapse (which
12077 // would silently absorb the `DuplicateNome` refusal case at the
12078 // accessor boundary).
12079 for deps in [
12080 vec![],
12081 vec![Dep::simple("", "^0.1")],
12082 vec![Dep::simple("caixa-teia", "^0.1")],
12083 vec![
12084 Dep::simple("caixa-teia", "^0.1"),
12085 Dep::simple("caixa-core", "^0.1"),
12086 ],
12087 vec![
12088 Dep::simple("caixa-teia", "^0.1"),
12089 Dep::simple("caixa-teia", "^0.2"),
12090 ],
12091 ] {
12092 let c = caixa_with_deps(deps.clone());
12093 assert_eq!(
12094 c.deps(),
12095 deps.as_slice(),
12096 "Caixa::deps must return :deps verbatim (got {:?}, \
12097 expected {deps:?})",
12098 c.deps(),
12099 );
12100 assert_eq!(
12101 c.deps(),
12102 c.deps.as_slice(),
12103 "Caixa::deps must element-equal the raw \
12104 `self.deps.as_slice()` field access across every \
12105 value in the Vec<Dep> accept-set",
12106 );
12107 }
12108 }
12109
12110 #[test]
12111 fn validate_deps_duplicate_arm_routes_through_accessor() {
12112 // Composition pin: [`Caixa::validate_deps`]'s within-`:deps`
12113 // duplicate-`:nome` gate must key off [`Caixa::deps`], not the
12114 // raw `&self.deps` field-borrow walk. Structurally: a `Caixa
12115 // { deps: vec![Dep::simple("d", "^0.1"), Dep::simple("d",
12116 // "^0.2")], .. }` must surface the `DuplicateNome { list:
12117 // ":deps" }` refusal exactly, and a `Caixa { deps: vec![
12118 // Dep::simple("d", "^0.1")], .. }` (the canonical single-entry
12119 // form) must pass validate. The pair jointly pins the accessor +
12120 // validate-gate composition: any future silent detour that had
12121 // the accessor return a dedupped slice on the `[a, a]` arm (a
12122 // `.iter().unique_by(|d| d.nome.as_str()).collect()` collapse)
12123 // would silently absorb the `DuplicateNome` refusal at the
12124 // accessor boundary and the validate gate would accept a
12125 // struct-literal `Caixa` carrying the drift — the composition
12126 // pin catches that at caixa-core build time.
12127 //
12128 // Peer of the per-`Caixa`
12129 // `validate_autores_empty_entry_arm_routes_through_accessor`
12130 // (b5d813f), `validate_etiquetas_empty_entry_arm_routes_through_accessor`
12131 // (78c7d3c), `validate_code_paths_bibliotecas_empty_arm_routes_through_accessor`
12132 // (8a36c23), `validate_code_paths_exe_empty_arm_routes_through_accessor`
12133 // (65d9527), and `validate_code_paths_servicos_empty_arm_routes_through_accessor`
12134 // (611f78b) accessor-composition pins on the sibling `&[T]`-
12135 // composition axes — same "the validate gate must route through
12136 // the substrate-primitive typed dispatch" discipline extended
12137 // onto the sibling outer top-level [`Caixa`] `&[Dep]`-
12138 // composition surface, opening the outer-`Caixa` dependency-slot
12139 // arm of the composition-pin family.
12140 let c = caixa_with_deps(vec![Dep::simple("d", "^0.1"), Dep::simple("d", "^0.2")]);
12141 let err = c.validate_deps().unwrap_err();
12142 assert!(
12143 matches!(
12144 err,
12145 DepError::DuplicateNome { ref nome, list } if nome == "d"
12146 && list == crate::render::DEP_AUTHOR_KEY_DEPS
12147 ),
12148 "validate_deps must reject deps == \
12149 vec![Dep(\"d\",\"^0.1\"), Dep(\"d\",\"^0.2\")] with \
12150 DuplicateNome {{ nome: \"d\", list: \":deps\" }} — the \
12151 accessor and the validate gate must route through the \
12152 same substrate-primitive typed dispatch on the :deps \
12153 within-list duplicate arm (got {err:?})",
12154 );
12155 let c = caixa_with_deps(vec![Dep::simple("d", "^0.1")]);
12156 assert!(
12157 c.validate_deps().is_ok(),
12158 "validate_deps must accept deps == vec![Dep(\"d\",\"^0.1\")] \
12159 (the canonical single-entry form)",
12160 );
12161 }
12162
12163 #[test]
12164 fn deps_projects_slice_by_borrow() {
12165 // The by-borrow pin: [`Caixa::deps`] returns `&[Dep]` by borrow
12166 // — the returned slice borrows the underlying `Vec<Dep>` storage
12167 // of the `:deps` slot and the accessor must not clone the
12168 // backing `Vec` on every call. Peer of the per-`Caixa`
12169 // `autores_projects_slice_by_borrow` (b5d813f),
12170 // `etiquetas_projects_slice_by_borrow` (78c7d3c),
12171 // `bibliotecas_projects_slice_by_borrow` (8a36c23),
12172 // `exe_projects_slice_by_borrow` (65d9527), and
12173 // `servicos_projects_slice_by_borrow` (611f78b) by-borrow pins
12174 // on the sibling outer top-level [`Caixa`] `&[String]`-return
12175 // axes — the accessor's returned slice must borrow from `&self`
12176 // (the returned reference's lifetime is tied to `&self`), and
12177 // calling the accessor twice on the same [`Caixa`] must yield
12178 // slices that are pointer-equal (the underlying byte-buffer is
12179 // the storage `Vec`'s allocation, not a fresh copy) as well as
12180 // value-equal (idempotent, no side effects on `&self`).
12181 //
12182 // Pins against a future silent detour that returned an owned
12183 // `Vec<Dep>` (which would type-check but silently clone on
12184 // every call), a `&Vec<Dep>` return (which would leak the
12185 // backing `Vec`'s grow/push/reserve surface no downstream
12186 // consumer reaches for), or a one-arm-only accessor that
12187 // returned a saturating value on some sentinel input.
12188 for deps in [
12189 vec![],
12190 vec![Dep::simple("caixa-teia", "^0.1")],
12191 vec![
12192 Dep::simple("caixa-teia", "^0.1"),
12193 Dep::simple("caixa-core", "^0.1"),
12194 ],
12195 ] {
12196 let c = caixa_with_deps(deps.clone());
12197 let first = c.deps();
12198 let second = c.deps();
12199 assert_eq!(
12200 first, second,
12201 "Caixa::deps must be idempotent — two successive calls \
12202 on the same &self must return the same &[Dep]",
12203 );
12204 assert_eq!(
12205 first.as_ptr(),
12206 second.as_ptr(),
12207 "Caixa::deps must borrow the underlying Vec<Dep> \
12208 storage — two successive calls must return slices \
12209 with the same backing pointer (a fresh Vec<Dep> clone \
12210 would change the pointer on every call)",
12211 );
12212 assert_eq!(
12213 first,
12214 deps.as_slice(),
12215 "Caixa::deps must return :deps verbatim by borrow — \
12216 got {first:?}, expected {deps:?}",
12217 );
12218 }
12219 }
12220
12221 // ── Caixa::deps_dev — outer top-level &[Dep] slice accessor ──────
12222
12223 fn caixa_with_deps_dev(deps_dev: Vec<Dep>) -> Caixa {
12224 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
12225 c.deps_dev = deps_dev;
12226 c
12227 }
12228
12229 #[test]
12230 fn deps_dev_returns_deps_dev_slice_verbatim_across_permutations() {
12231 // The canonical per-`Caixa` `:deps-dev` universal-axis dev-only-
12232 // dependency-declaration-list slice pin: [`Caixa::deps_dev`]
12233 // must return the `:deps-dev` typed [`Vec<Dep>`] list verbatim as
12234 // a `&[Dep]`, element-equal to the raw `self.deps_dev.as_slice()`
12235 // access across every representative value in the accept-set —
12236 // `[]` (the "no dev deps declared" arm every existing fixture
12237 // without a `:deps-dev` line carries; the [`Caixa::template`]
12238 // scaffold emits `:deps-dev ()`), a canonical single-entry list
12239 // (the shape most consumer caixas carry — a `tatara-check` dev
12240 // pin), a canonical two-entry list (the multi-dev-dep closure),
12241 // and two past-the-guard sentinels — a `[""]`-`:nome` entry
12242 // ([`Self::validate_deps`] rejects through `NomeEmpty` /
12243 // `NomeInvalid` but the accessor must ship the raw slot
12244 // verbatim) and a `[a, a]` duplicate (validate rejects through
12245 // `DuplicateNome { list: ":deps-dev" }` but the accessor must
12246 // ship the raw slot verbatim so struct-literal fixtures continue
12247 // to expose the duplicate at the accessor).
12248 //
12249 // Second outer top-level [`Caixa`] `&[Dep]`-return slice-accessor
12250 // pin on the substrate primitive — closes the outer-`Caixa`
12251 // dependency-slot `&[Dep]` sub-family the sibling
12252 // `deps_returns_deps_slice_verbatim_across_permutations`
12253 // (ad34b4e) opened on. Folds the "outer [`Caixa`] `&[Dep]`
12254 // slice" projection pattern onto the sibling dev-dep axis —
12255 // pins against a future silent detour that returned an owned
12256 // `Vec<Dep>` (which would type-check but silently clone on every
12257 // accessor call, breaking the zero-cost projection every peer
12258 // sibling slice accessor carries), a `[""] → []` collapse (which
12259 // would silently absorb the `NomeEmpty` refusal case at the
12260 // accessor boundary), or a `[a, a] → [a]` dedup collapse (which
12261 // would silently absorb the `DuplicateNome` refusal case at the
12262 // accessor boundary).
12263 for deps_dev in [
12264 vec![],
12265 vec![Dep::simple("", "^0.1")],
12266 vec![Dep::simple("tatara-check", "^0.1")],
12267 vec![
12268 Dep::simple("tatara-check", "^0.1"),
12269 Dep::simple("caixa-lint", "^0.1"),
12270 ],
12271 vec![
12272 Dep::simple("tatara-check", "^0.1"),
12273 Dep::simple("tatara-check", "^0.2"),
12274 ],
12275 ] {
12276 let c = caixa_with_deps_dev(deps_dev.clone());
12277 assert_eq!(
12278 c.deps_dev(),
12279 deps_dev.as_slice(),
12280 "Caixa::deps_dev must return :deps-dev verbatim (got \
12281 {:?}, expected {deps_dev:?})",
12282 c.deps_dev(),
12283 );
12284 assert_eq!(
12285 c.deps_dev(),
12286 c.deps_dev.as_slice(),
12287 "Caixa::deps_dev must element-equal the raw \
12288 `self.deps_dev.as_slice()` field access across every \
12289 value in the Vec<Dep> accept-set",
12290 );
12291 }
12292 }
12293
12294 #[test]
12295 fn validate_deps_duplicate_deps_dev_arm_routes_through_accessor() {
12296 // Composition pin: [`Caixa::validate_deps`]'s within-`:deps-dev`
12297 // duplicate-`:nome` gate must key off [`Caixa::deps_dev`], not
12298 // the raw `&self.deps_dev` field-borrow walk. Structurally: a
12299 // `Caixa { deps_dev: vec![Dep::simple("d", "^0.1"),
12300 // Dep::simple("d", "^0.2")], .. }` must surface the
12301 // `DuplicateNome { list: ":deps-dev" }` refusal exactly, and a
12302 // `Caixa { deps_dev: vec![Dep::simple("d", "^0.1")], .. }` (the
12303 // canonical single-entry form) must pass validate. The pair
12304 // jointly pins the accessor + validate-gate composition: any
12305 // future silent detour that had the accessor return a dedupped
12306 // slice on the `[a, a]` arm (a
12307 // `.iter().unique_by(|d| d.nome.as_str()).collect()` collapse)
12308 // would silently absorb the `DuplicateNome` refusal at the
12309 // accessor boundary and the validate gate would accept a
12310 // struct-literal `Caixa` carrying the drift — the composition
12311 // pin catches that at caixa-core build time.
12312 //
12313 // Peer of `validate_deps_duplicate_arm_routes_through_accessor`
12314 // (ad34b4e) on the sibling `:deps` axis — same "the validate
12315 // gate must route through the substrate-primitive typed
12316 // dispatch" discipline folded onto the sibling `:deps-dev`
12317 // axis, closing the two-list dep-graph composition-pin family.
12318 // The `:deps-dev` diagnostic must carry the
12319 // `DEP_AUTHOR_KEY_DEPS_DEV` list-tag (not
12320 // `DEP_AUTHOR_KEY_DEPS`) so the emitted error names the
12321 // offending list unambiguously.
12322 let c = caixa_with_deps_dev(vec![Dep::simple("d", "^0.1"), Dep::simple("d", "^0.2")]);
12323 let err = c.validate_deps().unwrap_err();
12324 assert!(
12325 matches!(
12326 err,
12327 DepError::DuplicateNome { ref nome, list } if nome == "d"
12328 && list == crate::render::DEP_AUTHOR_KEY_DEPS_DEV
12329 ),
12330 "validate_deps must reject deps_dev == \
12331 vec![Dep(\"d\",\"^0.1\"), Dep(\"d\",\"^0.2\")] with \
12332 DuplicateNome {{ nome: \"d\", list: \":deps-dev\" }} — the \
12333 accessor and the validate gate must route through the \
12334 same substrate-primitive typed dispatch on the :deps-dev \
12335 within-list duplicate arm (got {err:?})",
12336 );
12337 let c = caixa_with_deps_dev(vec![Dep::simple("d", "^0.1")]);
12338 assert!(
12339 c.validate_deps().is_ok(),
12340 "validate_deps must accept deps_dev == \
12341 vec![Dep(\"d\",\"^0.1\")] (the canonical single-entry form)",
12342 );
12343 }
12344
12345 #[test]
12346 fn deps_dev_projects_slice_by_borrow() {
12347 // The by-borrow pin: [`Caixa::deps_dev`] returns `&[Dep]` by
12348 // borrow — the returned slice borrows the underlying `Vec<Dep>`
12349 // storage of the `:deps-dev` slot and the accessor must not
12350 // clone the backing `Vec` on every call. Peer of
12351 // `deps_projects_slice_by_borrow` (ad34b4e) on the sibling
12352 // `:deps` axis, and of the per-`Caixa`
12353 // `autores_projects_slice_by_borrow` (b5d813f),
12354 // `etiquetas_projects_slice_by_borrow` (78c7d3c),
12355 // `bibliotecas_projects_slice_by_borrow` (8a36c23),
12356 // `exe_projects_slice_by_borrow` (65d9527), and
12357 // `servicos_projects_slice_by_borrow` (611f78b) by-borrow pins
12358 // on the sibling outer top-level [`Caixa`] `&[String]`-return
12359 // axes — the accessor's returned slice must borrow from `&self`
12360 // (the returned reference's lifetime is tied to `&self`), and
12361 // calling the accessor twice on the same [`Caixa`] must yield
12362 // slices that are pointer-equal (the underlying byte-buffer is
12363 // the storage `Vec`'s allocation, not a fresh copy) as well as
12364 // value-equal (idempotent, no side effects on `&self`).
12365 //
12366 // Pins against a future silent detour that returned an owned
12367 // `Vec<Dep>` (which would type-check but silently clone on
12368 // every call), a `&Vec<Dep>` return (which would leak the
12369 // backing `Vec`'s grow/push/reserve surface no downstream
12370 // consumer reaches for), or a one-arm-only accessor that
12371 // returned a saturating value on some sentinel input.
12372 for deps_dev in [
12373 vec![],
12374 vec![Dep::simple("tatara-check", "^0.1")],
12375 vec![
12376 Dep::simple("tatara-check", "^0.1"),
12377 Dep::simple("caixa-lint", "^0.1"),
12378 ],
12379 ] {
12380 let c = caixa_with_deps_dev(deps_dev.clone());
12381 let first = c.deps_dev();
12382 let second = c.deps_dev();
12383 assert_eq!(
12384 first, second,
12385 "Caixa::deps_dev must be idempotent — two successive \
12386 calls on the same &self must return the same &[Dep]",
12387 );
12388 assert_eq!(
12389 first.as_ptr(),
12390 second.as_ptr(),
12391 "Caixa::deps_dev must borrow the underlying Vec<Dep> \
12392 storage — two successive calls must return slices \
12393 with the same backing pointer (a fresh Vec<Dep> clone \
12394 would change the pointer on every call)",
12395 );
12396 assert_eq!(
12397 first,
12398 deps_dev.as_slice(),
12399 "Caixa::deps_dev must return :deps-dev verbatim by \
12400 borrow — got {first:?}, expected {deps_dev:?}",
12401 );
12402 }
12403 }
12404
12405 // ── Caixa::limits — outer top-level Option<&LimitsSpec> composite-reference accessor ──
12406
12407 fn caixa_with_limits(limits: Option<crate::LimitsSpec>) -> Caixa {
12408 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
12409 c.limits = limits;
12410 c
12411 }
12412
12413 #[test]
12414 fn limits_returns_limits_option_ref_verbatim_across_permutations() {
12415 // The canonical per-`Caixa` `:limits` M2 typed-slot outer-
12416 // composite optional-composite-reference-shape pin:
12417 // [`Caixa::limits`] must return the `:limits` typed
12418 // `Option<LimitsSpec>` verbatim as an `Option<&LimitsSpec>`
12419 // reference over the same backing storage the raw
12420 // `self.limits.as_ref()` field access borrows from, byte-equal
12421 // across every representative fixture in the accept-set — the
12422 // author-omitted `None` shape (the "engine-default applies"
12423 // partition every downstream Servico M2 overlay emitter treats
12424 // as "emit nothing"), the empty-composite `Some(LimitsSpec {
12425 // .. default })` shape ([`LimitsSpec::is_empty`] holds — every
12426 // per-axis cap is `None`, so the peer M2 overlay emitter's
12427 // `.is_empty()`-gated projection still emits nothing but the
12428 // outer presence-bit is `Some`, so [`Caixa::declared_servico_slots`]
12429 // still pushes the `M2_AUTHOR_KEY_LIMITS` label), a single-axis
12430 // fixture (only `:memory` set — the canonical shape most
12431 // memory-heavy Servicos carry), and a fully-populated composite
12432 // (every per-axis cap set — the canonical shape a
12433 // sandboxed-by-default Servico carries).
12434 //
12435 // Pins against a future silent detour that returned a fresh-
12436 // cloned [`LimitsSpec`] copy (which would type-check via the
12437 // `Clone` impl but silently break every downstream caller that
12438 // relied on the reference sharing the composite's backing
12439 // identity), a reference to an operator-resolved overlay (the
12440 // future per-cluster `:limits-overrides` slot — its resolution
12441 // must land at exactly this accessor body, not silently divert
12442 // the raw slot away from a second consumer), a
12443 // `None` → `Some(LimitsSpec::default)` cluster-default
12444 // projection (which would collapse the load-bearing
12445 // "author-omitted `:limits` ⇒ engine-default applies" partition
12446 // the peer [`crate::render::servico_m2_overlay`] emitter and
12447 // the peer [`Caixa::declared_servico_slots`] enumerator both
12448 // read), or an axis-shuffled projection (a future detour that
12449 // swapped `memory` and `fuel` through the accessor would
12450 // silently split the paired [`crate::StandardLayout::verify`]
12451 // per-`:limits` shape gate's traversal input from the peer
12452 // `servico_m2_overlay` emitter's projection input).
12453 //
12454 // First outer top-level [`Caixa`] `Option<&Composite>`-return
12455 // composite-reference accessor pin on the substrate primitive
12456 // — opens the outer-`Caixa` `Option<&Composite>` composite-
12457 // reference projection pattern the sibling `:behavior`
12458 // [`crate::BehaviorSpec`] / `:politicas`
12459 // [`crate::aplicacao::MeshPolicy`] / `:placement`
12460 // [`crate::aplicacao::Placement`] / `:entrada`
12461 // [`crate::aplicacao::Entrada`] future outer-composite lifts
12462 // fold on. Peer of the closed M3 outer-composite family the
12463 // sibling [`crate::AplicacaoSpec::politicas`] (534dc21) /
12464 // [`crate::AplicacaoSpec::placement`] (9abb8f0) /
12465 // [`crate::AplicacaoSpec::entrada`] (d32111c) composite-
12466 // reference accessor pins already carry on the outer
12467 // [`crate::AplicacaoSpec`] altitude — extends the outer-
12468 // accessor byte-equal-projection discipline onto the outer
12469 // top-level [`Caixa`] M2 Servico-runtime slot altitude.
12470 use crate::LimitsSpec;
12471 use std::time::Duration;
12472 let fixtures: Vec<Option<LimitsSpec>> = vec![
12473 None,
12474 Some(LimitsSpec::default()),
12475 Some(LimitsSpec {
12476 memory: Some(64 * 1024 * 1024),
12477 ..Default::default()
12478 }),
12479 Some(LimitsSpec {
12480 memory: Some(64 * 1024 * 1024),
12481 fuel: Some(1_000_000),
12482 wall_clock: Some(Duration::from_secs(30)),
12483 cpu: Some(500),
12484 }),
12485 ];
12486 for limits in fixtures {
12487 let c = caixa_with_limits(limits.clone());
12488 assert_eq!(
12489 c.limits(),
12490 limits.as_ref(),
12491 "Caixa::limits must return :limits verbatim (got {:?}, \
12492 expected {:?})",
12493 c.limits(),
12494 limits.as_ref(),
12495 );
12496 match (c.limits(), c.limits.as_ref()) {
12497 (Some(a), Some(b)) => assert!(
12498 std::ptr::eq(a, b),
12499 "Caixa::limits accessor and self.limits.as_ref() \
12500 field access must borrow the same backing storage \
12501 — the accessor is the substrate-primitive typed \
12502 dispatch every downstream Servico-M2-overlay \
12503 composite consumer must route through, and a \
12504 reference-identity split would silently break \
12505 every consumer that relied on the borrow sharing \
12506 the composite's storage",
12507 ),
12508 (None, None) => {}
12509 _ => panic!(
12510 "Caixa::limits presence bit must byte-equal \
12511 self.limits.is_some() — a presence-bit drift would \
12512 silently split the paired StandardLayout::verify \
12513 per-`:limits` shape gate's traversal head from \
12514 the peer render::servico_m2_overlay M2 overlay \
12515 emitter's traversal head from the peer \
12516 Caixa::declared_servico_slots M2 declared-slot \
12517 enumerator's presence probe",
12518 ),
12519 }
12520 assert_eq!(
12521 c.limits().is_some(),
12522 c.limits.is_some(),
12523 "Caixa::limits().is_some() must byte-equal \
12524 self.limits.is_some() — a presence-bit drift would \
12525 silently split every downstream Option<&LimitsSpec> \
12526 consumer's partition on the engine-default arm",
12527 );
12528 }
12529 }
12530
12531 #[test]
12532 fn declared_servico_slots_limits_arm_routes_through_accessor() {
12533 // Composition pin: [`Caixa::declared_servico_slots`]'s
12534 // `:limits` presence-probe arm must key off [`Caixa::limits`],
12535 // not the raw `self.limits.is_some()` field-probe. Structurally:
12536 // a `Caixa { limits: Some(LimitsSpec::default()), .. }` must
12537 // still push `M2_AUTHOR_KEY_LIMITS` onto the declared-slot list
12538 // (the presence bit is `Some`, so the M2 kind-coherence gate
12539 // must surface the slot as "declared" even when every per-axis
12540 // cap is unset), and a `Caixa { limits: None, .. }` must NOT
12541 // push the label (the "author omitted the slot entirely"
12542 // partition). The pair jointly pins the accessor + declared-
12543 // slot enumerator composition: any future silent detour that
12544 // had the accessor collapse `Some(LimitsSpec::default())` to
12545 // `None` (a `.filter(|l| !l.is_empty())` projection) would
12546 // silently absorb the "declared but empty" arm at the
12547 // accessor boundary and the [`crate::LayoutError::ServicoSlotsOnNonServico`]
12548 // kind-coherence gate would silently accept a
12549 // struct-literal `Caixa` carrying the drift.
12550 //
12551 // Peer of the sibling per-`Caixa`
12552 // `validate_deps_duplicate_arm_routes_through_accessor` (ad34b4e)
12553 // and `validate_deps_duplicate_deps_dev_arm_routes_through_accessor`
12554 // (f7fd81e) accessor-composition pins on the sibling `:deps` /
12555 // `:deps-dev` outer-`&[Dep]`-composition axes — same "the
12556 // enumerator gate must route through the substrate-primitive
12557 // typed dispatch" discipline extended onto the outer top-level
12558 // [`Caixa`] `Option<&LimitsSpec>`-composition surface, opening
12559 // the outer-`Caixa` M2 Servico-runtime-slot arm of the
12560 // composition-pin family.
12561 use crate::LimitsSpec;
12562 let c = caixa_with_limits(Some(LimitsSpec::default()));
12563 let slots = c.declared_servico_slots();
12564 assert!(
12565 slots.contains(&crate::render::M2_AUTHOR_KEY_LIMITS),
12566 "declared_servico_slots must push M2_AUTHOR_KEY_LIMITS \
12567 when `:limits` is Some (even for LimitsSpec::default()) \
12568 — the accessor and the enumerator gate must route through \
12569 the same substrate-primitive typed dispatch on the outer \
12570 :limits presence bit (got slots={slots:?})",
12571 );
12572 let c = caixa_with_limits(None);
12573 let slots = c.declared_servico_slots();
12574 assert!(
12575 !slots.contains(&crate::render::M2_AUTHOR_KEY_LIMITS),
12576 "declared_servico_slots must NOT push M2_AUTHOR_KEY_LIMITS \
12577 when `:limits` is None — the author-omitted arm must \
12578 route through the accessor's None-return unchanged (got \
12579 slots={slots:?})",
12580 );
12581 }
12582
12583 #[test]
12584 fn servico_m2_overlay_limits_arm_routes_through_accessor() {
12585 // Composition pin: [`crate::render::servico_m2_overlay`]'s
12586 // per-`:limits` M2 overlay emit arm must key off
12587 // [`Caixa::limits`], not the raw `&caixa.limits` field-borrow.
12588 // Structurally: a `Caixa { limits: Some(LimitsSpec { memory:
12589 // Some(64 MiB), .. default }), .. }` must surface the
12590 // `M2_KEY_LIMITS` key with the per-axis
12591 // `memory: "64MiB"` sub-mapping in the overlay, a `Caixa {
12592 // limits: Some(LimitsSpec::default()), .. }` must omit the
12593 // key entirely (the `.is_empty()`-gated inner arm elides an
12594 // empty composite even when the outer presence bit is `Some`),
12595 // and a `Caixa { limits: None, .. }` must also omit the key
12596 // (the "author omitted the slot entirely" partition). The
12597 // three-fixture family jointly pins the accessor + M2 overlay
12598 // emitter composition: any future silent detour that had the
12599 // accessor return a fresh-cloned copy on the `Some` arm (a
12600 // `LimitsSpec::clone()` projection) would silently break the
12601 // reference-identity pin the peer per-axis
12602 // `serde_yaml::to_value(limits)` projection reads from.
12603 use crate::LimitsSpec;
12604 use crate::render::{M2_KEY_LIMITS, servico_m2_overlay};
12605 let c = caixa_with_limits(Some(LimitsSpec {
12606 memory: Some(64 * 1024 * 1024),
12607 ..Default::default()
12608 }));
12609 let overlay = servico_m2_overlay(&c).unwrap();
12610 assert!(
12611 overlay.contains_key(M2_KEY_LIMITS),
12612 "servico_m2_overlay must surface M2_KEY_LIMITS when \
12613 `:limits` carries a non-empty composite — the accessor \
12614 and the M2 overlay emitter must route through the same \
12615 substrate-primitive typed dispatch on the outer :limits \
12616 composite (got overlay={overlay:?})",
12617 );
12618 let c = caixa_with_limits(Some(LimitsSpec::default()));
12619 let overlay = servico_m2_overlay(&c).unwrap();
12620 assert!(
12621 !overlay.contains_key(M2_KEY_LIMITS),
12622 "servico_m2_overlay must omit M2_KEY_LIMITS when \
12623 `:limits` is Some(LimitsSpec::default()) — the empty \
12624 composite's `.is_empty()`-gated inner arm must elide \
12625 the key regardless of the outer presence bit (got \
12626 overlay={overlay:?})",
12627 );
12628 let c = caixa_with_limits(None);
12629 let overlay = servico_m2_overlay(&c).unwrap();
12630 assert!(
12631 !overlay.contains_key(M2_KEY_LIMITS),
12632 "servico_m2_overlay must omit M2_KEY_LIMITS when \
12633 `:limits` is None — the author-omitted arm must route \
12634 through the accessor's None-return unchanged (got \
12635 overlay={overlay:?})",
12636 );
12637 }
12638
12639 #[test]
12640 fn limits_projects_option_ref_by_borrow() {
12641 // The by-borrow pin: [`Caixa::limits`] returns
12642 // `Option<&LimitsSpec>` by borrow — the returned reference
12643 // borrows the underlying `Option<LimitsSpec>` storage of the
12644 // `:limits` slot and the accessor must not clone the backing
12645 // composite on every call. Peer of the sibling
12646 // `deps_projects_slice_by_borrow` (ad34b4e) /
12647 // `deps_dev_projects_slice_by_borrow` (f7fd81e) by-borrow pins
12648 // on the outer top-level [`Caixa`] `&[Dep]`-return axes —
12649 // extended here to the outer [`Caixa`] `Option<&Composite>`-
12650 // return axis: the accessor's returned reference must borrow
12651 // from `&self` (the returned reference's lifetime is tied to
12652 // `&self`), and calling the accessor twice on the same
12653 // [`Caixa`] must yield references that are pointer-equal (the
12654 // underlying byte-buffer is the storage `LimitsSpec`'s
12655 // allocation, not a fresh copy) as well as value-equal
12656 // (idempotent, no side effects on `&self`).
12657 //
12658 // Pins against a future silent detour that returned an owned
12659 // `LimitsSpec` (which would type-check via the `Clone` impl
12660 // but silently clone on every call), a `&LimitsSpec` panic-
12661 // return on the `None` arm (which would collapse the load-
12662 // bearing `Option` presence-bit into a runtime panic), or a
12663 // one-arm-only accessor that returned a saturating composite
12664 // on some sentinel input.
12665 use crate::LimitsSpec;
12666 use std::time::Duration;
12667 for limits in [
12668 Some(LimitsSpec::default()),
12669 Some(LimitsSpec {
12670 memory: Some(64 * 1024 * 1024),
12671 fuel: Some(1_000_000),
12672 wall_clock: Some(Duration::from_secs(30)),
12673 cpu: Some(500),
12674 }),
12675 ] {
12676 let c = caixa_with_limits(limits.clone());
12677 let first = c.limits().unwrap();
12678 let second = c.limits().unwrap();
12679 assert_eq!(
12680 first, second,
12681 "Caixa::limits must be idempotent — two successive \
12682 calls on the same &self must return the same \
12683 &LimitsSpec",
12684 );
12685 assert!(
12686 std::ptr::eq(first, second),
12687 "Caixa::limits must borrow the underlying \
12688 Option<LimitsSpec> storage — two successive calls \
12689 must return references with the same backing pointer \
12690 (a fresh LimitsSpec clone would change the pointer \
12691 on every call)",
12692 );
12693 assert_eq!(
12694 Some(first),
12695 limits.as_ref(),
12696 "Caixa::limits must return :limits verbatim by borrow \
12697 — got {first:?}, expected {:?}",
12698 limits.as_ref(),
12699 );
12700 }
12701 let c = caixa_with_limits(None);
12702 assert!(
12703 c.limits().is_none(),
12704 "Caixa::limits must return None when :limits is absent — \
12705 the author-omitted arm must project through the \
12706 accessor's Option::None unchanged",
12707 );
12708 }
12709
12710 // ── Caixa::behavior — outer top-level Option<&BehaviorSpec> composite-reference accessor ──
12711
12712 fn caixa_with_behavior(behavior: Option<crate::BehaviorSpec>) -> Caixa {
12713 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
12714 c.behavior = behavior;
12715 c
12716 }
12717
12718 #[test]
12719 fn behavior_returns_behavior_option_ref_verbatim_across_permutations() {
12720 // The canonical per-`Caixa` `:behavior` M2 typed-slot outer-
12721 // composite optional-composite-reference-shape pin:
12722 // [`Caixa::behavior`] must return the `:behavior` typed
12723 // `Option<BehaviorSpec>` verbatim as an `Option<&BehaviorSpec>`
12724 // reference over the same backing storage the raw
12725 // `self.behavior.as_ref()` field access borrows from, byte-equal
12726 // across every representative fixture in the accept-set — the
12727 // author-omitted `None` shape (the "runtime-default applies"
12728 // partition every downstream Servico M2 overlay emitter treats
12729 // as "emit nothing"), the empty-composite `Some(BehaviorSpec {
12730 // .. default })` shape ([`BehaviorSpec::is_empty`] holds —
12731 // every per-callback path is `None`, so the peer M2 overlay
12732 // emitter's `.is_empty()`-gated projection still emits nothing
12733 // but the outer presence-bit is `Some`, so
12734 // [`Caixa::declared_servico_slots`] still pushes the
12735 // `M2_AUTHOR_KEY_BEHAVIOR` label), a single-callback fixture
12736 // (only `:on-state-change` set — the canonical shape a caixa
12737 // that only wires the hot-upgrade migration path carries), and
12738 // a fully-populated composite (every per-callback path set —
12739 // the canonical shape a fully-instrumented gen_server-shaped
12740 // Servico carries).
12741 //
12742 // Peer of the sibling
12743 // `limits_returns_limits_option_ref_verbatim_across_permutations`
12744 // (b2bd9d7) opening fixture-family + reference-identity +
12745 // presence-bit tetrad pin on the outer top-level [`Caixa`]
12746 // `Option<&Composite>`-return sub-family — extended here to the
12747 // second axis of that sub-family so both of the currently-lifted
12748 // M2 Servico-runtime `Option<&Composite>` slots (`:limits` /
12749 // `:behavior`) carry the same "byte-equal, borrow-shared,
12750 // presence-bit-preserved" outer-accessor discipline.
12751 //
12752 // Pins against a future silent detour that returned a fresh-
12753 // cloned [`crate::BehaviorSpec`] copy (which would type-check
12754 // via the `Clone` impl but silently break every downstream
12755 // caller that relied on the reference sharing the composite's
12756 // backing identity), a reference to an operator-resolved
12757 // overlay (a future per-cluster `:behavior-overrides` slot —
12758 // its resolution must land at exactly this accessor body, not
12759 // silently divert the raw slot away from a second consumer), a
12760 // `None` → `Some(BehaviorSpec::default)` cluster-default
12761 // projection (which would collapse the load-bearing
12762 // "author-omitted `:behavior` ⇒ runtime-default applies"
12763 // partition the peer [`crate::render::servico_m2_overlay`]
12764 // emitter, the peer [`Caixa::declared_servico_slots`]
12765 // enumerator, and the cross-slot
12766 // [`crate::upgrade::validate_upgrade_from_against_behavior`]
12767 // gate all read), or a callback-shuffled projection (a future
12768 // detour that swapped `on_init` and `on_terminate` through the
12769 // accessor would silently split the paired
12770 // [`crate::StandardLayout::verify`] per-`:behavior` shape gate's
12771 // traversal input from the peer `servico_m2_overlay` emitter's
12772 // projection input from the cross-slot `:state-change`
12773 // composition gate's traversal input).
12774 use crate::BehaviorSpec;
12775 use std::path::PathBuf;
12776 let fixtures: Vec<Option<BehaviorSpec>> = vec![
12777 None,
12778 Some(BehaviorSpec::default()),
12779 Some(BehaviorSpec {
12780 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
12781 ..Default::default()
12782 }),
12783 Some(BehaviorSpec {
12784 on_init: Some(PathBuf::from("lib/init.lisp")),
12785 on_call: Some(PathBuf::from("lib/handlers.lisp")),
12786 on_cast: Some(PathBuf::from("lib/handlers.lisp")),
12787 on_info: Some(PathBuf::from("lib/handlers.lisp")),
12788 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
12789 on_terminate: Some(PathBuf::from("lib/cleanup.lisp")),
12790 }),
12791 ];
12792 for behavior in fixtures {
12793 let c = caixa_with_behavior(behavior.clone());
12794 assert_eq!(
12795 c.behavior(),
12796 behavior.as_ref(),
12797 "Caixa::behavior must return :behavior verbatim (got \
12798 {:?}, expected {:?})",
12799 c.behavior(),
12800 behavior.as_ref(),
12801 );
12802 match (c.behavior(), c.behavior.as_ref()) {
12803 (Some(a), Some(b)) => assert!(
12804 std::ptr::eq(a, b),
12805 "Caixa::behavior accessor and self.behavior.as_ref() \
12806 field access must borrow the same backing storage \
12807 — the accessor is the substrate-primitive typed \
12808 dispatch every downstream Servico-M2-overlay \
12809 composite consumer must route through, and a \
12810 reference-identity split would silently break \
12811 every consumer that relied on the borrow sharing \
12812 the composite's storage",
12813 ),
12814 (None, None) => {}
12815 _ => panic!(
12816 "Caixa::behavior presence bit must byte-equal \
12817 self.behavior.is_some() — a presence-bit drift \
12818 would silently split the paired \
12819 StandardLayout::verify per-`:behavior` shape \
12820 gate's traversal head from the peer \
12821 render::servico_m2_overlay M2 overlay emitter's \
12822 traversal head from the cross-slot \
12823 validate_upgrade_from_against_behavior \
12824 composition gate's traversal head from the peer \
12825 Caixa::declared_servico_slots M2 declared-slot \
12826 enumerator's presence probe",
12827 ),
12828 }
12829 assert_eq!(
12830 c.behavior().is_some(),
12831 c.behavior.is_some(),
12832 "Caixa::behavior().is_some() must byte-equal \
12833 self.behavior.is_some() — a presence-bit drift would \
12834 silently split every downstream Option<&BehaviorSpec> \
12835 consumer's partition on the runtime-default arm",
12836 );
12837 }
12838 }
12839
12840 #[test]
12841 fn declared_servico_slots_behavior_arm_routes_through_accessor() {
12842 // Composition pin: [`Caixa::declared_servico_slots`]'s
12843 // `:behavior` presence-probe arm must key off
12844 // [`Caixa::behavior`], not the raw `self.behavior.is_some()`
12845 // field-probe. Structurally: a `Caixa { behavior:
12846 // Some(BehaviorSpec::default()), .. }` must still push
12847 // `M2_AUTHOR_KEY_BEHAVIOR` onto the declared-slot list (the
12848 // presence bit is `Some`, so the M2 kind-coherence gate must
12849 // surface the slot as "declared" even when every per-callback
12850 // path is unset), and a `Caixa { behavior: None, .. }` must
12851 // NOT push the label (the "author omitted the slot entirely"
12852 // partition). The pair jointly pins the accessor + declared-
12853 // slot enumerator composition: any future silent detour that
12854 // had the accessor collapse `Some(BehaviorSpec::default())`
12855 // to `None` (a `.filter(|b| !b.is_empty())` projection) would
12856 // silently absorb the "declared but empty" arm at the
12857 // accessor boundary and the
12858 // [`crate::LayoutError::ServicoSlotsOnNonServico`]
12859 // kind-coherence gate would silently accept a struct-literal
12860 // `Caixa` carrying the drift.
12861 //
12862 // Peer of the sibling
12863 // `declared_servico_slots_limits_arm_routes_through_accessor`
12864 // (b2bd9d7) composition pin on the sibling `:limits` outer-
12865 // `Option<&LimitsSpec>` arm of the same
12866 // [`Caixa::declared_servico_slots`] M2 declared-slot
12867 // enumerator's traversal — same "the enumerator gate must
12868 // route through the substrate-primitive typed dispatch"
12869 // discipline extended onto the outer top-level [`Caixa`]
12870 // `Option<&BehaviorSpec>`-composition surface.
12871 use crate::BehaviorSpec;
12872 let c = caixa_with_behavior(Some(BehaviorSpec::default()));
12873 let slots = c.declared_servico_slots();
12874 assert!(
12875 slots.contains(&crate::render::M2_AUTHOR_KEY_BEHAVIOR),
12876 "declared_servico_slots must push M2_AUTHOR_KEY_BEHAVIOR \
12877 when `:behavior` is Some (even for BehaviorSpec::default()) \
12878 — the accessor and the enumerator gate must route through \
12879 the same substrate-primitive typed dispatch on the outer \
12880 :behavior presence bit (got slots={slots:?})",
12881 );
12882 let c = caixa_with_behavior(None);
12883 let slots = c.declared_servico_slots();
12884 assert!(
12885 !slots.contains(&crate::render::M2_AUTHOR_KEY_BEHAVIOR),
12886 "declared_servico_slots must NOT push M2_AUTHOR_KEY_BEHAVIOR \
12887 when `:behavior` is None — the author-omitted arm must \
12888 route through the accessor's None-return unchanged (got \
12889 slots={slots:?})",
12890 );
12891 }
12892
12893 #[test]
12894 fn servico_m2_overlay_behavior_arm_routes_through_accessor() {
12895 // Composition pin: [`crate::render::servico_m2_overlay`]'s
12896 // per-`:behavior` M2 overlay emit arm must key off
12897 // [`Caixa::behavior`], not the raw `&caixa.behavior`
12898 // field-borrow. Structurally: a `Caixa { behavior:
12899 // Some(BehaviorSpec { on_state_change: Some(...), .. default
12900 // }), .. }` must surface the `M2_KEY_BEHAVIOR` key with the
12901 // per-callback `onStateChange` sub-mapping in the overlay, a
12902 // `Caixa { behavior: Some(BehaviorSpec::default()), .. }`
12903 // must omit the key entirely (the `.is_empty()`-gated inner
12904 // arm elides an empty composite even when the outer presence
12905 // bit is `Some`), and a `Caixa { behavior: None, .. }` must
12906 // also omit the key (the "author omitted the slot entirely"
12907 // partition). The three-fixture family jointly pins the
12908 // accessor + M2 overlay emitter composition: any future
12909 // silent detour that had the accessor return a fresh-cloned
12910 // copy on the `Some` arm (a `BehaviorSpec::clone()`
12911 // projection) would silently break the reference-identity
12912 // pin the peer per-callback `serde_yaml::to_value(behavior)`
12913 // projection reads from.
12914 //
12915 // Peer of the sibling
12916 // `servico_m2_overlay_limits_arm_routes_through_accessor`
12917 // (b2bd9d7) composition pin on the sibling `:limits` outer-
12918 // `Option<&LimitsSpec>` arm of the same
12919 // [`crate::render::servico_m2_overlay`] M2 overlay emitter's
12920 // traversal — same "the emitter must route through the
12921 // substrate-primitive typed dispatch on the outer composite"
12922 // discipline extended onto the outer top-level [`Caixa`]
12923 // `Option<&BehaviorSpec>`-composition surface.
12924 use crate::BehaviorSpec;
12925 use crate::render::{M2_KEY_BEHAVIOR, servico_m2_overlay};
12926 use std::path::PathBuf;
12927 let c = caixa_with_behavior(Some(BehaviorSpec {
12928 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
12929 ..Default::default()
12930 }));
12931 let overlay = servico_m2_overlay(&c).unwrap();
12932 assert!(
12933 overlay.contains_key(M2_KEY_BEHAVIOR),
12934 "servico_m2_overlay must surface M2_KEY_BEHAVIOR when \
12935 `:behavior` carries a non-empty composite — the accessor \
12936 and the M2 overlay emitter must route through the same \
12937 substrate-primitive typed dispatch on the outer :behavior \
12938 composite (got overlay={overlay:?})",
12939 );
12940 let c = caixa_with_behavior(Some(BehaviorSpec::default()));
12941 let overlay = servico_m2_overlay(&c).unwrap();
12942 assert!(
12943 !overlay.contains_key(M2_KEY_BEHAVIOR),
12944 "servico_m2_overlay must omit M2_KEY_BEHAVIOR when \
12945 `:behavior` is Some(BehaviorSpec::default()) — the empty \
12946 composite's `.is_empty()`-gated inner arm must elide the \
12947 key regardless of the outer presence bit (got \
12948 overlay={overlay:?})",
12949 );
12950 let c = caixa_with_behavior(None);
12951 let overlay = servico_m2_overlay(&c).unwrap();
12952 assert!(
12953 !overlay.contains_key(M2_KEY_BEHAVIOR),
12954 "servico_m2_overlay must omit M2_KEY_BEHAVIOR when \
12955 `:behavior` is None — the author-omitted arm must route \
12956 through the accessor's None-return unchanged (got \
12957 overlay={overlay:?})",
12958 );
12959 }
12960
12961 #[test]
12962 fn behavior_projects_option_ref_by_borrow() {
12963 // The by-borrow pin: [`Caixa::behavior`] returns
12964 // `Option<&BehaviorSpec>` by borrow — the returned reference
12965 // borrows the underlying `Option<BehaviorSpec>` storage of the
12966 // `:behavior` slot and the accessor must not clone the backing
12967 // composite on every call. Peer of the sibling
12968 // `limits_projects_option_ref_by_borrow` (b2bd9d7) by-borrow
12969 // pin on the outer top-level [`Caixa`] `Option<&Composite>`-
12970 // return sub-family — extended here to the second axis of the
12971 // same sub-family: the accessor's returned reference must
12972 // borrow from `&self` (the returned reference's lifetime is
12973 // tied to `&self`), and calling the accessor twice on the same
12974 // [`Caixa`] must yield references that are pointer-equal (the
12975 // underlying byte-buffer is the storage `BehaviorSpec`'s
12976 // allocation, not a fresh copy) as well as value-equal
12977 // (idempotent, no side effects on `&self`).
12978 //
12979 // Pins against a future silent detour that returned an owned
12980 // `BehaviorSpec` (which would type-check via the `Clone` impl
12981 // but silently clone on every call), a `&BehaviorSpec` panic-
12982 // return on the `None` arm (which would collapse the load-
12983 // bearing `Option` presence-bit into a runtime panic), or a
12984 // one-arm-only accessor that returned a saturating composite
12985 // on some sentinel input.
12986 use crate::BehaviorSpec;
12987 use std::path::PathBuf;
12988 for behavior in [
12989 Some(BehaviorSpec::default()),
12990 Some(BehaviorSpec {
12991 on_init: Some(PathBuf::from("lib/init.lisp")),
12992 on_call: Some(PathBuf::from("lib/handlers.lisp")),
12993 on_cast: Some(PathBuf::from("lib/handlers.lisp")),
12994 on_info: Some(PathBuf::from("lib/handlers.lisp")),
12995 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
12996 on_terminate: Some(PathBuf::from("lib/cleanup.lisp")),
12997 }),
12998 ] {
12999 let c = caixa_with_behavior(behavior.clone());
13000 let first = c.behavior().unwrap();
13001 let second = c.behavior().unwrap();
13002 assert_eq!(
13003 first, second,
13004 "Caixa::behavior must be idempotent — two successive \
13005 calls on the same &self must return the same \
13006 &BehaviorSpec",
13007 );
13008 assert!(
13009 std::ptr::eq(first, second),
13010 "Caixa::behavior must borrow the underlying \
13011 Option<BehaviorSpec> storage — two successive calls \
13012 must return references with the same backing pointer \
13013 (a fresh BehaviorSpec clone would change the pointer \
13014 on every call)",
13015 );
13016 assert_eq!(
13017 Some(first),
13018 behavior.as_ref(),
13019 "Caixa::behavior must return :behavior verbatim by \
13020 borrow — got {first:?}, expected {:?}",
13021 behavior.as_ref(),
13022 );
13023 }
13024 let c = caixa_with_behavior(None);
13025 assert!(
13026 c.behavior().is_none(),
13027 "Caixa::behavior must return None when :behavior is absent \
13028 — the author-omitted arm must project through the \
13029 accessor's Option::None unchanged",
13030 );
13031 }
13032
13033 // ── Caixa::politicas — outer top-level Option<&MeshPolicy> composite-reference accessor ──
13034
13035 fn caixa_aplicacao_with_politicas(politicas: Option<crate::aplicacao::MeshPolicy>) -> Caixa {
13036 use crate::aplicacao::{Membro, WitContract};
13037 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
13038 c.kind = CaixaKind::Aplicacao;
13039 c.membros = vec![Membro {
13040 caixa: "a".into(),
13041 versao: "^0.1".into(),
13042 }];
13043 c.contratos = vec![WitContract {
13044 de: "a".into(),
13045 para: "a".into(),
13046 wit: "wasi:http/proxy".into(),
13047 endpoint: Some("/x".into()),
13048 subject: None,
13049 slot: None,
13050 }];
13051 c.politicas = politicas;
13052 c
13053 }
13054
13055 #[test]
13056 fn politicas_returns_politicas_option_ref_verbatim_across_permutations() {
13057 // The canonical per-`Caixa` `:politicas` M3 mesh-slot outer-
13058 // composite optional-composite-reference-shape pin:
13059 // [`Caixa::politicas`] must return the `:politicas` typed
13060 // `Option<MeshPolicy>` verbatim as an `Option<&MeshPolicy>`
13061 // reference over the same backing storage the raw
13062 // `self.politicas.as_ref()` field access borrows from,
13063 // byte-equal across every representative fixture in the
13064 // accept-set — the author-omitted `None` shape (the "cluster-
13065 // default applies" partition every downstream mesh-artifact
13066 // emitter treats as "emit no `:politicas` overlay"), the
13067 // empty-composite `Some(MeshPolicy { .. default })` shape
13068 // ([`crate::aplicacao::MeshPolicy::is_empty`] holds — every
13069 // per-axis mesh-policy scalar is `None`, so the peer inner
13070 // [`crate::AplicacaoSpec::politicas`] `.is_empty()`-gated
13071 // caixa-mesh overlay elides every per-axis emit but the outer
13072 // presence-bit is `Some`, so [`Caixa::declared_mesh_slots`]
13073 // still pushes the `M3_AUTHOR_KEY_POLITICAS` label), a
13074 // single-axis fixture (only `:timeout` set — the canonical
13075 // shape a latency-sensitive Aplicacao carries), and a
13076 // fully-populated composite (every per-axis mesh-policy
13077 // scalar set — the canonical shape a fully-governed
13078 // Aplicacao carries).
13079 //
13080 // Pins against a future silent detour that returned a fresh-
13081 // cloned [`crate::aplicacao::MeshPolicy`] copy (which would
13082 // type-check via the `Clone` impl but silently break every
13083 // downstream caller that relied on the reference sharing the
13084 // composite's backing identity), a reference to an operator-
13085 // resolved overlay (the future per-cluster
13086 // `:politicas-overrides` slot — its resolution must land at
13087 // exactly this accessor body, not silently divert the raw
13088 // slot away from the peer [`Caixa::declared_mesh_slots`]
13089 // enumerator's presence probe), a
13090 // `None` → `Some(MeshPolicy::default)` cluster-default
13091 // projection (which would collapse the load-bearing
13092 // "author-omitted `:politicas` ⇒ cluster-default applies"
13093 // partition the peer [`Caixa::declared_mesh_slots`]
13094 // enumerator and the peer [`Caixa::aplicacao_view`]
13095 // Aplicacao-composition seed both read), or an axis-shuffled
13096 // projection (a future detour that swapped `timeout` and
13097 // `retries` through the accessor would silently split the
13098 // paired [`Caixa::aplicacao_view`] seed's fold input from the
13099 // sibling M3 mesh-artifact emitter's projection input).
13100 //
13101 // Third outer top-level [`Caixa`] `Option<&Composite>`-return
13102 // composite-reference accessor pin on the substrate primitive
13103 // — peer of the sibling
13104 // `limits_returns_limits_option_ref_verbatim_across_permutations`
13105 // (b2bd9d7) and
13106 // `behavior_returns_behavior_option_ref_verbatim_across_permutations`
13107 // (35d8b52) opening tetrad pins on the outer top-level
13108 // [`Caixa`] `Option<&Composite>`-return sub-family — extended
13109 // here to the first of the three M3 mesh-slot axes so the
13110 // opening third of the outer `Option<&Composite>` sub-family
13111 // carries the same "byte-equal, borrow-shared, presence-bit-
13112 // preserved" outer-accessor discipline.
13113 use crate::aplicacao::{CircuitBreaker, MeshPolicy, RateLimit};
13114 use std::time::Duration;
13115 let fixtures: Vec<Option<MeshPolicy>> = vec![
13116 None,
13117 Some(MeshPolicy::default()),
13118 Some(MeshPolicy {
13119 timeout: Some(Duration::from_secs(30)),
13120 ..Default::default()
13121 }),
13122 Some(MeshPolicy {
13123 timeout: Some(Duration::from_secs(30)),
13124 retries: Some(3),
13125 circuit_breaker: Some(CircuitBreaker {
13126 max_failures: 5,
13127 window: Duration::from_secs(60),
13128 }),
13129 mtls_required: Some(true),
13130 rate_limit: Some(RateLimit {
13131 rate: 100,
13132 window: Duration::from_secs(1),
13133 }),
13134 }),
13135 ];
13136 for politicas in fixtures {
13137 let c = caixa_aplicacao_with_politicas(politicas.clone());
13138 assert_eq!(
13139 c.politicas(),
13140 politicas.as_ref(),
13141 "Caixa::politicas must return :politicas verbatim (got \
13142 {:?}, expected {:?})",
13143 c.politicas(),
13144 politicas.as_ref(),
13145 );
13146 match (c.politicas(), c.politicas.as_ref()) {
13147 (Some(a), Some(b)) => assert!(
13148 std::ptr::eq(a, b),
13149 "Caixa::politicas accessor and self.politicas.as_ref() \
13150 field access must borrow the same backing storage \
13151 — the accessor is the substrate-primitive typed \
13152 dispatch every downstream Aplicacao-mesh-overlay \
13153 composite consumer must route through, and a \
13154 reference-identity split would silently break \
13155 every consumer that relied on the borrow sharing \
13156 the composite's storage",
13157 ),
13158 (None, None) => {}
13159 _ => panic!(
13160 "Caixa::politicas presence bit must byte-equal \
13161 self.politicas.is_some() — a presence-bit drift \
13162 would silently split the paired \
13163 Caixa::aplicacao_view Aplicacao-composition seed's \
13164 traversal head from the peer \
13165 Caixa::declared_mesh_slots M3 declared-slot \
13166 enumerator's presence probe",
13167 ),
13168 }
13169 assert_eq!(
13170 c.politicas().is_some(),
13171 c.politicas.is_some(),
13172 "Caixa::politicas().is_some() must byte-equal \
13173 self.politicas.is_some() — a presence-bit drift would \
13174 silently split every downstream Option<&MeshPolicy> \
13175 consumer's partition on the cluster-default arm",
13176 );
13177 }
13178 }
13179
13180 #[test]
13181 fn declared_mesh_slots_politicas_arm_routes_through_accessor() {
13182 // Composition pin: [`Caixa::declared_mesh_slots`]'s
13183 // `:politicas` presence-probe arm must key off
13184 // [`Caixa::politicas`], not the raw `self.politicas.is_some()`
13185 // field-probe. Structurally: a `Caixa { politicas:
13186 // Some(MeshPolicy::default()), .. }` must still push
13187 // `M3_AUTHOR_KEY_POLITICAS` onto the declared-slot list (the
13188 // presence bit is `Some`, so the M3 kind-coherence gate must
13189 // surface the slot as "declared" even when every per-axis
13190 // scalar is unset), and a `Caixa { politicas: None, .. }` must
13191 // NOT push the label (the "author omitted the slot entirely"
13192 // partition). The pair jointly pins the accessor + declared-
13193 // slot enumerator composition: any future silent detour that
13194 // had the accessor collapse `Some(MeshPolicy::default())` to
13195 // `None` (a `.filter(|p| !p.is_empty())` projection) would
13196 // silently absorb the "declared but empty" arm at the
13197 // accessor boundary and the
13198 // [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
13199 // coherence gate would silently accept a struct-literal
13200 // `Caixa` carrying the drift.
13201 //
13202 // Peer of the sibling
13203 // `declared_servico_slots_limits_arm_routes_through_accessor`
13204 // (b2bd9d7) and
13205 // `declared_servico_slots_behavior_arm_routes_through_accessor`
13206 // (35d8b52) composition pins on the sibling `:limits` /
13207 // `:behavior` outer-`Option<&Composite>` arms of the peer
13208 // [`Caixa::declared_servico_slots`] M2 declared-slot
13209 // enumerator's traversal — same "the enumerator gate must
13210 // route through the substrate-primitive typed dispatch"
13211 // discipline extended onto the outer top-level [`Caixa`] M3
13212 // mesh-slot family so the [`Caixa::declared_mesh_slots`]
13213 // enumerator carries the same routing invariant as its M2
13214 // sibling.
13215 use crate::aplicacao::MeshPolicy;
13216 let c = caixa_aplicacao_with_politicas(Some(MeshPolicy::default()));
13217 let slots = c.declared_mesh_slots();
13218 assert!(
13219 slots.contains(&crate::render::M3_AUTHOR_KEY_POLITICAS),
13220 "declared_mesh_slots must push M3_AUTHOR_KEY_POLITICAS \
13221 when `:politicas` is Some (even for MeshPolicy::default()) \
13222 — the accessor and the enumerator gate must route through \
13223 the same substrate-primitive typed dispatch on the outer \
13224 :politicas presence bit (got slots={slots:?})",
13225 );
13226 let c = caixa_aplicacao_with_politicas(None);
13227 let slots = c.declared_mesh_slots();
13228 assert!(
13229 !slots.contains(&crate::render::M3_AUTHOR_KEY_POLITICAS),
13230 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_POLITICAS \
13231 when `:politicas` is None — the author-omitted arm must \
13232 route through the accessor's None-return unchanged (got \
13233 slots={slots:?})",
13234 );
13235 }
13236
13237 #[test]
13238 fn aplicacao_view_politicas_arm_folds_through_accessor() {
13239 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:politicas`
13240 // Aplicacao-composition seed must fold through
13241 // [`Caixa::politicas`], not the raw
13242 // `self.politicas.clone().unwrap_or_default()` field-borrow.
13243 // Structurally: a `Caixa { politicas: Some(MeshPolicy {
13244 // timeout: Some(30s), .. default }), kind: Aplicacao, .. }`
13245 // must surface a projected [`crate::AplicacaoSpec`] whose
13246 // `politicas().timeout()` field byte-equals the outer
13247 // composite's `timeout` scalar (the fold must project the
13248 // authored composite verbatim), a `Caixa { politicas:
13249 // Some(MeshPolicy::default()), kind: Aplicacao, .. }` must
13250 // surface an [`crate::AplicacaoSpec`] whose `politicas()`
13251 // byte-equals [`crate::aplicacao::MeshPolicy::default`] (the
13252 // fold's empty-composite arm collapses to the same default the
13253 // author-omitted arm does), and a `Caixa { politicas: None,
13254 // kind: Aplicacao, .. }` must surface an
13255 // [`crate::AplicacaoSpec`] whose `politicas()` byte-equals
13256 // [`crate::aplicacao::MeshPolicy::default`] (the "author
13257 // omitted the slot entirely" arm folds through the
13258 // `unwrap_or_default` onto the cluster-default). The triad
13259 // jointly pins the accessor + Aplicacao-composition seed
13260 // composition: any future silent detour that had the accessor
13261 // divert the raw slot away from the seed's fold (an operator-
13262 // resolved overlay's default-fold arm silently differing from
13263 // the raw slot's default-fold arm) would silently split the
13264 // build-time mesh-artifact emission gate from the caixa-mesh
13265 // renderer's Aplicacao-view input at the composition boundary.
13266 use crate::aplicacao::MeshPolicy;
13267 use std::time::Duration;
13268 let c = caixa_aplicacao_with_politicas(Some(MeshPolicy {
13269 timeout: Some(Duration::from_secs(30)),
13270 ..Default::default()
13271 }));
13272 let view = c.aplicacao_view().unwrap();
13273 assert_eq!(
13274 view.politicas().timeout(),
13275 Some(Duration::from_secs(30)),
13276 "Caixa::aplicacao_view must fold the authored :politicas \
13277 :timeout scalar through the accessor verbatim onto the \
13278 projected AplicacaoSpec — a future silent detour at the \
13279 seed's fold arm would surface here as a projected-scalar \
13280 drift (got {:?})",
13281 view.politicas().timeout(),
13282 );
13283 let c = caixa_aplicacao_with_politicas(Some(MeshPolicy::default()));
13284 let view = c.aplicacao_view().unwrap();
13285 assert_eq!(
13286 view.politicas(),
13287 &MeshPolicy::default(),
13288 "Caixa::aplicacao_view must fold Some(MeshPolicy::default()) \
13289 through the accessor onto MeshPolicy::default — the empty- \
13290 composite arm collapses to the same default the author- \
13291 omitted arm does (got {:?})",
13292 view.politicas(),
13293 );
13294 let c = caixa_aplicacao_with_politicas(None);
13295 let view = c.aplicacao_view().unwrap();
13296 assert_eq!(
13297 view.politicas(),
13298 &MeshPolicy::default(),
13299 "Caixa::aplicacao_view must fold None through the accessor's \
13300 unwrap_or_default onto MeshPolicy::default — the author- \
13301 omitted arm must route through the accessor's None-return \
13302 unchanged (got {:?})",
13303 view.politicas(),
13304 );
13305 }
13306
13307 #[test]
13308 fn politicas_projects_option_ref_by_borrow() {
13309 // The by-borrow pin: [`Caixa::politicas`] returns
13310 // `Option<&MeshPolicy>` by borrow — the returned reference
13311 // borrows the underlying `Option<MeshPolicy>` storage of the
13312 // `:politicas` slot and the accessor must not clone the
13313 // backing composite on every call. Peer of the sibling
13314 // `limits_projects_option_ref_by_borrow` (b2bd9d7) and
13315 // `behavior_projects_option_ref_by_borrow` (35d8b52) by-borrow
13316 // pins on the outer top-level [`Caixa`]
13317 // `Option<&Composite>`-return sub-family — extended here to
13318 // the third axis of the same sub-family: the accessor's
13319 // returned reference must borrow from `&self` (the returned
13320 // reference's lifetime is tied to `&self`), and calling the
13321 // accessor twice on the same [`Caixa`] must yield references
13322 // that are pointer-equal (the underlying byte-buffer is the
13323 // storage `MeshPolicy`'s allocation, not a fresh copy) as
13324 // well as value-equal (idempotent, no side effects on
13325 // `&self`).
13326 //
13327 // Pins against a future silent detour that returned an owned
13328 // `MeshPolicy` (which would type-check via the `Clone` impl
13329 // but silently clone on every call), a `&MeshPolicy` panic-
13330 // return on the `None` arm (which would collapse the load-
13331 // bearing `Option` presence-bit into a runtime panic), or a
13332 // one-arm-only accessor that returned a saturating composite
13333 // on some sentinel input.
13334 use crate::aplicacao::{CircuitBreaker, MeshPolicy, RateLimit};
13335 use std::time::Duration;
13336 for politicas in [
13337 Some(MeshPolicy::default()),
13338 Some(MeshPolicy {
13339 timeout: Some(Duration::from_secs(30)),
13340 retries: Some(3),
13341 circuit_breaker: Some(CircuitBreaker {
13342 max_failures: 5,
13343 window: Duration::from_secs(60),
13344 }),
13345 mtls_required: Some(true),
13346 rate_limit: Some(RateLimit {
13347 rate: 100,
13348 window: Duration::from_secs(1),
13349 }),
13350 }),
13351 ] {
13352 let c = caixa_aplicacao_with_politicas(politicas.clone());
13353 let first = c.politicas().unwrap();
13354 let second = c.politicas().unwrap();
13355 assert_eq!(
13356 first, second,
13357 "Caixa::politicas must be idempotent — two successive \
13358 calls on the same &self must return the same \
13359 &MeshPolicy",
13360 );
13361 assert!(
13362 std::ptr::eq(first, second),
13363 "Caixa::politicas must borrow the underlying \
13364 Option<MeshPolicy> storage — two successive calls \
13365 must return references with the same backing pointer \
13366 (a fresh MeshPolicy clone would change the pointer on \
13367 every call)",
13368 );
13369 assert_eq!(
13370 Some(first),
13371 politicas.as_ref(),
13372 "Caixa::politicas must return :politicas verbatim by \
13373 borrow — got {first:?}, expected {:?}",
13374 politicas.as_ref(),
13375 );
13376 }
13377 let c = caixa_aplicacao_with_politicas(None);
13378 assert!(
13379 c.politicas().is_none(),
13380 "Caixa::politicas must return None when :politicas is \
13381 absent — the author-omitted arm must project through the \
13382 accessor's Option::None unchanged",
13383 );
13384 }
13385
13386 // ── Caixa::placement — outer top-level Option<&Placement> composite-reference accessor ──
13387
13388 fn caixa_aplicacao_with_placement(placement: Option<crate::aplicacao::Placement>) -> Caixa {
13389 use crate::aplicacao::{Membro, WitContract};
13390 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
13391 c.kind = CaixaKind::Aplicacao;
13392 c.membros = vec![Membro {
13393 caixa: "a".into(),
13394 versao: "^0.1".into(),
13395 }];
13396 c.contratos = vec![WitContract {
13397 de: "a".into(),
13398 para: "a".into(),
13399 wit: "wasi:http/proxy".into(),
13400 endpoint: Some("/x".into()),
13401 subject: None,
13402 slot: None,
13403 }];
13404 c.placement = placement;
13405 c
13406 }
13407
13408 #[test]
13409 fn placement_returns_placement_option_ref_verbatim_across_permutations() {
13410 // The canonical per-`Caixa` `:placement` M3 mesh-slot outer-
13411 // composite optional-composite-reference-shape pin:
13412 // [`Caixa::placement`] must return the `:placement` typed
13413 // `Option<Placement>` verbatim as an `Option<&Placement>`
13414 // reference over the same backing storage the raw
13415 // `self.placement.as_ref()` field access borrows from,
13416 // byte-equal across every representative fixture in the
13417 // accept-set — the author-omitted `None` shape (the
13418 // "cluster-default applies" partition every downstream mesh-
13419 // artifact emitter treats as "emit no `:placement` overlay"),
13420 // the empty-composite `Some(Placement { .. default })` shape
13421 // (`estrategia: SingleNode`, empty clusters, no shard-key /
13422 // affinity — the outer presence-bit is `Some` so
13423 // [`Caixa::declared_mesh_slots`] still pushes the
13424 // `M3_AUTHOR_KEY_PLACEMENT` label), a single-axis
13425 // `Replicated`-on-two-clusters fixture (the canonical shape a
13426 // stateless HTTP Aplicacao carries), and a fully-populated
13427 // `Sharded`-with-shard-key-and-affinity fixture (the canonical
13428 // shape a stateful Akka-style cluster-sharding Aplicacao
13429 // carries).
13430 //
13431 // Pins against a future silent detour that returned a fresh-
13432 // cloned [`crate::aplicacao::Placement`] copy (which would
13433 // type-check via the `Clone` impl but silently break every
13434 // downstream caller that relied on the reference sharing the
13435 // composite's backing identity), a reference to an operator-
13436 // resolved overlay (the future per-cluster
13437 // `:placement-overrides` slot — its resolution must land at
13438 // exactly this accessor body, not silently divert the raw
13439 // slot away from the peer [`Caixa::declared_mesh_slots`]
13440 // enumerator's presence probe), a `None` →
13441 // `Some(Placement::default)` cluster-default projection (which
13442 // would collapse the load-bearing "author-omitted `:placement`
13443 // ⇒ cluster-default applies" partition the peer
13444 // [`Caixa::declared_mesh_slots`] enumerator and the peer
13445 // [`Caixa::aplicacao_view`] Aplicacao-composition seed both
13446 // read), or an axis-shuffled projection (a future detour that
13447 // swapped `clusters` and `affinity` through the accessor would
13448 // silently split the paired [`Caixa::aplicacao_view`] seed's
13449 // fold input from the sibling M3 mesh-artifact emitter's
13450 // projection input).
13451 //
13452 // Fourth outer top-level [`Caixa`] `Option<&Composite>`-return
13453 // composite-reference accessor pin on the substrate primitive
13454 // — peer of the sibling
13455 // `limits_returns_limits_option_ref_verbatim_across_permutations`
13456 // (b2bd9d7),
13457 // `behavior_returns_behavior_option_ref_verbatim_across_permutations`
13458 // (35d8b52), and
13459 // `politicas_returns_politicas_option_ref_verbatim_across_permutations`
13460 // (5d23d29) opening triad pins on the outer top-level
13461 // [`Caixa`] `Option<&Composite>`-return sub-family — extended
13462 // here to the second of the three M3 mesh-slot axes so the
13463 // opening four-fifths of the outer `Option<&Composite>` sub-
13464 // family carries the same "byte-equal, borrow-shared,
13465 // presence-bit-preserved" outer-accessor discipline.
13466 use crate::aplicacao::{Placement, PlacementStrategy};
13467 let fixtures: Vec<Option<Placement>> = vec![
13468 None,
13469 Some(Placement::default()),
13470 Some(Placement {
13471 estrategia: PlacementStrategy::Replicated,
13472 clusters: vec!["rio".into(), "sao-paulo".into()],
13473 affinity: None,
13474 shard_key: None,
13475 }),
13476 Some(Placement {
13477 estrategia: PlacementStrategy::Sharded,
13478 clusters: vec!["rio".into(), "sao-paulo".into(), "brasilia".into()],
13479 affinity: Some("data-locality".into()),
13480 shard_key: Some("$tenantId".into()),
13481 }),
13482 ];
13483 for placement in fixtures {
13484 let c = caixa_aplicacao_with_placement(placement.clone());
13485 assert_eq!(
13486 c.placement(),
13487 placement.as_ref(),
13488 "Caixa::placement must return :placement verbatim (got \
13489 {:?}, expected {:?})",
13490 c.placement(),
13491 placement.as_ref(),
13492 );
13493 match (c.placement(), c.placement.as_ref()) {
13494 (Some(a), Some(b)) => assert!(
13495 std::ptr::eq(a, b),
13496 "Caixa::placement accessor and self.placement.as_ref() \
13497 field access must borrow the same backing storage \
13498 — the accessor is the substrate-primitive typed \
13499 dispatch every downstream Aplicacao-distribution- \
13500 overlay composite consumer must route through, and \
13501 a reference-identity split would silently break \
13502 every consumer that relied on the borrow sharing \
13503 the composite's storage",
13504 ),
13505 (None, None) => {}
13506 _ => panic!(
13507 "Caixa::placement presence bit must byte-equal \
13508 self.placement.is_some() — a presence-bit drift \
13509 would silently split the paired \
13510 Caixa::aplicacao_view Aplicacao-composition seed's \
13511 traversal head from the peer \
13512 Caixa::declared_mesh_slots M3 declared-slot \
13513 enumerator's presence probe",
13514 ),
13515 }
13516 assert_eq!(
13517 c.placement().is_some(),
13518 c.placement.is_some(),
13519 "Caixa::placement().is_some() must byte-equal \
13520 self.placement.is_some() — a presence-bit drift would \
13521 silently split every downstream Option<&Placement> \
13522 consumer's partition on the cluster-default arm",
13523 );
13524 }
13525 }
13526
13527 #[test]
13528 fn declared_mesh_slots_placement_arm_routes_through_accessor() {
13529 // Composition pin: [`Caixa::declared_mesh_slots`]'s
13530 // `:placement` presence-probe arm must key off
13531 // [`Caixa::placement`], not the raw `self.placement.is_some()`
13532 // field-probe. Structurally: a `Caixa { placement:
13533 // Some(Placement::default()), .. }` must still push
13534 // `M3_AUTHOR_KEY_PLACEMENT` onto the declared-slot list (the
13535 // presence bit is `Some`, so the M3 kind-coherence gate must
13536 // surface the slot as "declared" even when every per-axis
13537 // scalar defers to the cluster-default arm), and a `Caixa {
13538 // placement: None, .. }` must NOT push the label (the "author
13539 // omitted the slot entirely" partition). The pair jointly pins
13540 // the accessor + declared-slot enumerator composition: any
13541 // future silent detour that had the accessor collapse
13542 // `Some(Placement::default())` to `None` (a `.filter(|p|
13543 // p.clusters().is_empty().not())` projection) would silently
13544 // absorb the "declared but empty" arm at the accessor boundary
13545 // and the [`crate::LayoutError::MeshSlotsOnNonAplicacao`]
13546 // kind-coherence gate would silently accept a struct-literal
13547 // `Caixa` carrying the drift.
13548 //
13549 // Peer of the sibling
13550 // `declared_servico_slots_limits_arm_routes_through_accessor`
13551 // (b2bd9d7),
13552 // `declared_servico_slots_behavior_arm_routes_through_accessor`
13553 // (35d8b52), and
13554 // `declared_mesh_slots_politicas_arm_routes_through_accessor`
13555 // (5d23d29) composition pins on the sibling `:limits` /
13556 // `:behavior` / `:politicas` outer-`Option<&Composite>` arms
13557 // — same "the enumerator gate must route through the
13558 // substrate-primitive typed dispatch" discipline extended onto
13559 // the second of the three M3 mesh-slot axes so the
13560 // [`Caixa::declared_mesh_slots`] enumerator carries the same
13561 // routing invariant on the `:placement` arm as the peer
13562 // `:politicas` arm.
13563 use crate::aplicacao::Placement;
13564 let c = caixa_aplicacao_with_placement(Some(Placement::default()));
13565 let slots = c.declared_mesh_slots();
13566 assert!(
13567 slots.contains(&crate::render::M3_AUTHOR_KEY_PLACEMENT),
13568 "declared_mesh_slots must push M3_AUTHOR_KEY_PLACEMENT \
13569 when `:placement` is Some (even for Placement::default()) \
13570 — the accessor and the enumerator gate must route through \
13571 the same substrate-primitive typed dispatch on the outer \
13572 :placement presence bit (got slots={slots:?})",
13573 );
13574 let c = caixa_aplicacao_with_placement(None);
13575 let slots = c.declared_mesh_slots();
13576 assert!(
13577 !slots.contains(&crate::render::M3_AUTHOR_KEY_PLACEMENT),
13578 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_PLACEMENT \
13579 when `:placement` is None — the author-omitted arm must \
13580 route through the accessor's None-return unchanged (got \
13581 slots={slots:?})",
13582 );
13583 }
13584
13585 #[test]
13586 fn aplicacao_view_placement_arm_folds_through_accessor() {
13587 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:placement`
13588 // Aplicacao-composition seed must fold through
13589 // [`Caixa::placement`], not the raw
13590 // `self.placement.clone().unwrap_or_default()` field-borrow.
13591 // Structurally: a `Caixa { placement: Some(Placement {
13592 // estrategia: Replicated, clusters: ["rio"], .. default }),
13593 // kind: Aplicacao, .. }` must surface a projected
13594 // [`crate::AplicacaoSpec`] whose `placement().estrategia()` +
13595 // `placement().clusters()` byte-equal the outer composite's
13596 // authored values (the fold must project the authored
13597 // composite verbatim), a `Caixa { placement:
13598 // Some(Placement::default()), kind: Aplicacao, .. }` must
13599 // surface an [`crate::AplicacaoSpec`] whose `placement()`
13600 // byte-equals [`crate::aplicacao::Placement::default`] (the
13601 // fold's empty-composite arm collapses to the same default
13602 // the author-omitted arm does), and a `Caixa { placement:
13603 // None, kind: Aplicacao, .. }` must surface an
13604 // [`crate::AplicacaoSpec`] whose `placement()` byte-equals
13605 // [`crate::aplicacao::Placement::default`] (the "author
13606 // omitted the slot entirely" arm folds through the
13607 // `unwrap_or_default` onto the cluster-default). The triad
13608 // jointly pins the accessor + Aplicacao-composition seed
13609 // composition: any future silent detour that had the accessor
13610 // divert the raw slot away from the seed's fold (an operator-
13611 // resolved overlay's default-fold arm silently differing from
13612 // the raw slot's default-fold arm) would silently split the
13613 // build-time distribution-artifact emission gate from the
13614 // caixa-mesh renderer's Aplicacao-view input at the
13615 // composition boundary.
13616 use crate::aplicacao::{Placement, PlacementStrategy};
13617 let c = caixa_aplicacao_with_placement(Some(Placement {
13618 estrategia: PlacementStrategy::Replicated,
13619 clusters: vec!["rio".into()],
13620 affinity: None,
13621 shard_key: None,
13622 }));
13623 let view = c.aplicacao_view().unwrap();
13624 assert_eq!(
13625 view.placement().estrategia(),
13626 PlacementStrategy::Replicated,
13627 "Caixa::aplicacao_view must fold the authored :placement \
13628 :estrategia scalar through the accessor verbatim onto the \
13629 projected AplicacaoSpec — a future silent detour at the \
13630 seed's fold arm would surface here as a projected-scalar \
13631 drift (got {:?})",
13632 view.placement().estrategia(),
13633 );
13634 assert_eq!(
13635 view.placement().clusters(),
13636 &["rio"],
13637 "Caixa::aplicacao_view must fold the authored :placement \
13638 :clusters list through the accessor verbatim onto the \
13639 projected AplicacaoSpec — a future silent detour at the \
13640 seed's fold arm would surface here as a projected-list \
13641 drift (got {:?})",
13642 view.placement().clusters(),
13643 );
13644 let c = caixa_aplicacao_with_placement(Some(Placement::default()));
13645 let view = c.aplicacao_view().unwrap();
13646 assert_eq!(
13647 view.placement(),
13648 &Placement::default(),
13649 "Caixa::aplicacao_view must fold Some(Placement::default()) \
13650 through the accessor onto Placement::default — the empty- \
13651 composite arm collapses to the same default the author- \
13652 omitted arm does (got {:?})",
13653 view.placement(),
13654 );
13655 let c = caixa_aplicacao_with_placement(None);
13656 let view = c.aplicacao_view().unwrap();
13657 assert_eq!(
13658 view.placement(),
13659 &Placement::default(),
13660 "Caixa::aplicacao_view must fold None through the accessor's \
13661 unwrap_or_default onto Placement::default — the author- \
13662 omitted arm must route through the accessor's None-return \
13663 unchanged (got {:?})",
13664 view.placement(),
13665 );
13666 }
13667
13668 #[test]
13669 fn placement_projects_option_ref_by_borrow() {
13670 // The by-borrow pin: [`Caixa::placement`] returns
13671 // `Option<&Placement>` by borrow — the returned reference
13672 // borrows the underlying `Option<Placement>` storage of the
13673 // `:placement` slot and the accessor must not clone the
13674 // backing composite on every call. Peer of the sibling
13675 // `limits_projects_option_ref_by_borrow` (b2bd9d7),
13676 // `behavior_projects_option_ref_by_borrow` (35d8b52), and
13677 // `politicas_projects_option_ref_by_borrow` (5d23d29) by-borrow
13678 // pins on the outer top-level [`Caixa`]
13679 // `Option<&Composite>`-return sub-family — extended here to
13680 // the fourth axis of the same sub-family: the accessor's
13681 // returned reference must borrow from `&self` (the returned
13682 // reference's lifetime is tied to `&self`), and calling the
13683 // accessor twice on the same [`Caixa`] must yield references
13684 // that are pointer-equal (the underlying byte-buffer is the
13685 // storage `Placement`'s allocation, not a fresh copy) as well
13686 // as value-equal (idempotent, no side effects on `&self`).
13687 //
13688 // Pins against a future silent detour that returned an owned
13689 // `Placement` (which would type-check via the `Clone` impl
13690 // but silently clone on every call), a `&Placement` panic-
13691 // return on the `None` arm (which would collapse the load-
13692 // bearing `Option` presence-bit into a runtime panic), or a
13693 // one-arm-only accessor that returned a saturating composite
13694 // on some sentinel input.
13695 use crate::aplicacao::{Placement, PlacementStrategy};
13696 for placement in [
13697 Some(Placement::default()),
13698 Some(Placement {
13699 estrategia: PlacementStrategy::Sharded,
13700 clusters: vec!["rio".into(), "sao-paulo".into()],
13701 affinity: Some("data-locality".into()),
13702 shard_key: Some("$tenantId".into()),
13703 }),
13704 ] {
13705 let c = caixa_aplicacao_with_placement(placement.clone());
13706 let first = c.placement().unwrap();
13707 let second = c.placement().unwrap();
13708 assert_eq!(
13709 first, second,
13710 "Caixa::placement must be idempotent — two successive \
13711 calls on the same &self must return the same \
13712 &Placement",
13713 );
13714 assert!(
13715 std::ptr::eq(first, second),
13716 "Caixa::placement must borrow the underlying \
13717 Option<Placement> storage — two successive calls \
13718 must return references with the same backing pointer \
13719 (a fresh Placement clone would change the pointer on \
13720 every call)",
13721 );
13722 assert_eq!(
13723 Some(first),
13724 placement.as_ref(),
13725 "Caixa::placement must return :placement verbatim by \
13726 borrow — got {first:?}, expected {:?}",
13727 placement.as_ref(),
13728 );
13729 }
13730 let c = caixa_aplicacao_with_placement(None);
13731 assert!(
13732 c.placement().is_none(),
13733 "Caixa::placement must return None when :placement is \
13734 absent — the author-omitted arm must project through the \
13735 accessor's Option::None unchanged",
13736 );
13737 }
13738
13739 // ── Caixa::entrada — outer top-level Option<&Entrada> composite-reference accessor ──
13740
13741 fn caixa_aplicacao_with_entrada(entrada: Option<crate::aplicacao::Entrada>) -> Caixa {
13742 use crate::aplicacao::{Membro, WitContract};
13743 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
13744 c.kind = CaixaKind::Aplicacao;
13745 c.membros = vec![Membro {
13746 caixa: "a".into(),
13747 versao: "^0.1".into(),
13748 }];
13749 c.contratos = vec![WitContract {
13750 de: "a".into(),
13751 para: "a".into(),
13752 wit: "wasi:http/proxy".into(),
13753 endpoint: Some("/x".into()),
13754 subject: None,
13755 slot: None,
13756 }];
13757 c.entrada = entrada;
13758 c
13759 }
13760
13761 #[test]
13762 fn entrada_returns_entrada_option_ref_verbatim_across_permutations() {
13763 // The canonical per-`Caixa` `:entrada` M3 mesh-slot outer-
13764 // composite optional-composite-reference-shape pin:
13765 // [`Caixa::entrada`] must return the `:entrada` typed
13766 // `Option<Entrada>` verbatim as an `Option<&Entrada>`
13767 // reference over the same backing storage the raw
13768 // `self.entrada.as_ref()` field access borrows from,
13769 // byte-equal across every representative fixture in the
13770 // accept-set — the author-omitted `None` shape (the
13771 // "cluster-internal Aplicacao" partition every downstream
13772 // Gateway-API emitter treats as "emit no listener + no
13773 // HTTPRoute"), a bare-`host`/`para` minimum-composite fixture
13774 // (empty `paths` — the resolved-paths fallback the peer
13775 // [`crate::aplicacao::Entrada::resolved_paths`] cascade folds
13776 // onto the substrate catch-all), and a fully-populated
13777 // multi-path-with-non-default-port fixture (the canonical
13778 // shape a public HTTP Aplicacao carries).
13779 //
13780 // Pins against a future silent detour that returned a fresh-
13781 // cloned [`crate::aplicacao::Entrada`] copy (which would
13782 // type-check via the `Clone` impl but silently break every
13783 // downstream caller that relied on the reference sharing the
13784 // composite's backing identity), a reference to an operator-
13785 // resolved overlay (the future per-cluster
13786 // `:entrada-overrides` slot — its resolution must land at
13787 // exactly this accessor body, not silently divert the raw
13788 // slot away from the peer [`Caixa::declared_mesh_slots`]
13789 // enumerator's presence probe), or an axis-shuffled projection
13790 // (a future detour that swapped `host` and `para` through the
13791 // accessor would silently split the paired
13792 // [`Caixa::aplicacao_view`] seed's forward input from the
13793 // sibling M3 gateway-artifact emitter's projection input).
13794 //
13795 // Fifth and final outer top-level [`Caixa`]
13796 // `Option<&Composite>`-return composite-reference accessor pin
13797 // on the substrate primitive — peer of the sibling
13798 // `limits_returns_limits_option_ref_verbatim_across_permutations`
13799 // (b2bd9d7),
13800 // `behavior_returns_behavior_option_ref_verbatim_across_permutations`
13801 // (35d8b52),
13802 // `politicas_returns_politicas_option_ref_verbatim_across_permutations`
13803 // (5d23d29), and
13804 // `placement_returns_placement_option_ref_verbatim_across_permutations`
13805 // (4fb8074) opening tetrad pins on the outer top-level
13806 // [`Caixa`] `Option<&Composite>`-return sub-family — extended
13807 // here to the third and final M3 mesh-slot axis so the closed
13808 // outer `Option<&Composite>` sub-family carries the same
13809 // "byte-equal, borrow-shared, presence-bit-preserved" outer-
13810 // accessor discipline across all five arms.
13811 use crate::aplicacao::Entrada;
13812 let fixtures: Vec<Option<Entrada>> = vec![
13813 None,
13814 Some(Entrada {
13815 host: "checkout.quero.cloud".into(),
13816 para: "gateway".into(),
13817 paths: Vec::new(),
13818 port: crate::DEFAULT_SERVICO_PORT,
13819 }),
13820 Some(Entrada {
13821 host: "api.pleme.io".into(),
13822 para: "public-api".into(),
13823 paths: vec!["/v1".into(), "/v2".into()],
13824 port: 8080,
13825 }),
13826 ];
13827 for entrada in fixtures {
13828 let c = caixa_aplicacao_with_entrada(entrada.clone());
13829 assert_eq!(
13830 c.entrada(),
13831 entrada.as_ref(),
13832 "Caixa::entrada must return :entrada verbatim (got \
13833 {:?}, expected {:?})",
13834 c.entrada(),
13835 entrada.as_ref(),
13836 );
13837 match (c.entrada(), c.entrada.as_ref()) {
13838 (Some(a), Some(b)) => assert!(
13839 std::ptr::eq(a, b),
13840 "Caixa::entrada accessor and self.entrada.as_ref() \
13841 field access must borrow the same backing storage \
13842 — the accessor is the substrate-primitive typed \
13843 dispatch every downstream Aplicacao-external- \
13844 gateway composite consumer must route through, and \
13845 a reference-identity split would silently break \
13846 every consumer that relied on the borrow sharing \
13847 the composite's storage",
13848 ),
13849 (None, None) => {}
13850 _ => panic!(
13851 "Caixa::entrada presence bit must byte-equal \
13852 self.entrada.is_some() — a presence-bit drift \
13853 would silently split the paired \
13854 Caixa::aplicacao_view Aplicacao-composition seed's \
13855 traversal head from the peer \
13856 Caixa::declared_mesh_slots M3 declared-slot \
13857 enumerator's presence probe",
13858 ),
13859 }
13860 assert_eq!(
13861 c.entrada().is_some(),
13862 c.entrada.is_some(),
13863 "Caixa::entrada().is_some() must byte-equal \
13864 self.entrada.is_some() — a presence-bit drift would \
13865 silently split every downstream Option<&Entrada> \
13866 consumer's partition on the cluster-internal arm",
13867 );
13868 }
13869 }
13870
13871 #[test]
13872 fn declared_mesh_slots_entrada_arm_routes_through_accessor() {
13873 // Composition pin: [`Caixa::declared_mesh_slots`]'s `:entrada`
13874 // presence-probe arm must key off [`Caixa::entrada`], not the
13875 // raw `self.entrada.is_some()` field-probe. Structurally: a
13876 // `Caixa { entrada: Some(Entrada { host: "...", para: "...",
13877 // paths: [], port: DEFAULT_SERVICO_PORT }), .. }` must push
13878 // `M3_AUTHOR_KEY_ENTRADA` onto the declared-slot list (the
13879 // presence bit is `Some`, so the M3 kind-coherence gate must
13880 // surface the slot as "declared" even when every per-axis
13881 // scalar defers to the substrate catch-all / default port),
13882 // and a `Caixa { entrada: None, .. }` must NOT push the label
13883 // (the "author omitted the slot entirely" partition). The pair
13884 // jointly pins the accessor + declared-slot enumerator
13885 // composition: any future silent detour that had the accessor
13886 // collapse `Some(Entrada { paths: [], .. })` to `None` (a
13887 // `.filter(|e| !e.paths.is_empty())` projection) would silently
13888 // absorb the "declared but empty-paths" arm at the accessor
13889 // boundary and the
13890 // [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
13891 // coherence gate would silently accept a struct-literal
13892 // `Caixa` carrying the drift.
13893 //
13894 // Peer of the sibling
13895 // `declared_servico_slots_limits_arm_routes_through_accessor`
13896 // (b2bd9d7),
13897 // `declared_servico_slots_behavior_arm_routes_through_accessor`
13898 // (35d8b52),
13899 // `declared_mesh_slots_politicas_arm_routes_through_accessor`
13900 // (5d23d29), and
13901 // `declared_mesh_slots_placement_arm_routes_through_accessor`
13902 // (4fb8074) composition pins on the sibling `:limits` /
13903 // `:behavior` / `:politicas` / `:placement` outer-
13904 // `Option<&Composite>` arms — same "the enumerator gate must
13905 // route through the substrate-primitive typed dispatch"
13906 // discipline extended onto the third and final M3 mesh-slot
13907 // axis so the [`Caixa::declared_mesh_slots`] enumerator now
13908 // carries the routing invariant on every M3 mesh-slot arm.
13909 use crate::aplicacao::Entrada;
13910 let c = caixa_aplicacao_with_entrada(Some(Entrada {
13911 host: "checkout.quero.cloud".into(),
13912 para: "gateway".into(),
13913 paths: Vec::new(),
13914 port: crate::DEFAULT_SERVICO_PORT,
13915 }));
13916 let slots = c.declared_mesh_slots();
13917 assert!(
13918 slots.contains(&crate::render::M3_AUTHOR_KEY_ENTRADA),
13919 "declared_mesh_slots must push M3_AUTHOR_KEY_ENTRADA when \
13920 `:entrada` is Some (even for empty-paths / default-port) \
13921 — the accessor and the enumerator gate must route through \
13922 the same substrate-primitive typed dispatch on the outer \
13923 :entrada presence bit (got slots={slots:?})",
13924 );
13925 let c = caixa_aplicacao_with_entrada(None);
13926 let slots = c.declared_mesh_slots();
13927 assert!(
13928 !slots.contains(&crate::render::M3_AUTHOR_KEY_ENTRADA),
13929 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_ENTRADA \
13930 when `:entrada` is None — the author-omitted arm must \
13931 route through the accessor's None-return unchanged (got \
13932 slots={slots:?})",
13933 );
13934 }
13935
13936 #[test]
13937 fn aplicacao_view_entrada_arm_folds_through_accessor() {
13938 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:entrada`
13939 // Aplicacao-composition seed must fold through
13940 // [`Caixa::entrada`], not the raw `self.entrada.clone()` field-
13941 // borrow. Structurally: a `Caixa { entrada: Some(Entrada {
13942 // host: "api.pleme.io", para: "public-api", paths: ["/v1"],
13943 // port: 8080 }), kind: Aplicacao, .. }` must surface a projected
13944 // [`crate::AplicacaoSpec`] whose `entrada().unwrap()` byte-
13945 // equals the outer composite's authored value (the fold must
13946 // project the authored composite verbatim), and a `Caixa {
13947 // entrada: None, kind: Aplicacao, .. }` must surface an
13948 // [`crate::AplicacaoSpec`] whose `entrada()` is `None` (the
13949 // "author omitted the slot entirely" arm folds through the
13950 // accessor's `Option::cloned` onto the same `None` presence
13951 // bit — unlike the peer `:politicas` / `:placement` arms
13952 // `:entrada` has no cluster-default fold, the omitted arm
13953 // stays omitted). The pair jointly pins the accessor +
13954 // Aplicacao-composition seed composition: any future silent
13955 // detour that had the accessor divert the raw slot away from
13956 // the seed's fold (an operator-resolved overlay's forward arm
13957 // silently differing from the raw slot's forward arm) would
13958 // silently split the build-time gateway-artifact emission gate
13959 // from the caixa-mesh renderer's Aplicacao-view input at the
13960 // composition boundary.
13961 use crate::aplicacao::Entrada;
13962 let authored = Entrada {
13963 host: "api.pleme.io".into(),
13964 para: "public-api".into(),
13965 paths: vec!["/v1".into()],
13966 port: 8080,
13967 };
13968 let c = caixa_aplicacao_with_entrada(Some(authored.clone()));
13969 let view = c.aplicacao_view().unwrap();
13970 assert_eq!(
13971 view.entrada(),
13972 Some(&authored),
13973 "Caixa::aplicacao_view must fold the authored :entrada \
13974 composite through the accessor verbatim onto the \
13975 projected AplicacaoSpec — a future silent detour at the \
13976 seed's fold arm would surface here as a projected- \
13977 composite drift (got {:?})",
13978 view.entrada(),
13979 );
13980 let c = caixa_aplicacao_with_entrada(None);
13981 let view = c.aplicacao_view().unwrap();
13982 assert!(
13983 view.entrada().is_none(),
13984 "Caixa::aplicacao_view must fold None through the \
13985 accessor's Option::cloned onto None — the author- \
13986 omitted arm must route through the accessor's None-return \
13987 unchanged (got {:?})",
13988 view.entrada(),
13989 );
13990 }
13991
13992 #[test]
13993 fn entrada_projects_option_ref_by_borrow() {
13994 // The by-borrow pin: [`Caixa::entrada`] returns
13995 // `Option<&Entrada>` by borrow — the returned reference
13996 // borrows the underlying `Option<Entrada>` storage of the
13997 // `:entrada` slot and the accessor must not clone the backing
13998 // composite on every call. Peer of the sibling
13999 // `limits_projects_option_ref_by_borrow` (b2bd9d7),
14000 // `behavior_projects_option_ref_by_borrow` (35d8b52),
14001 // `politicas_projects_option_ref_by_borrow` (5d23d29), and
14002 // `placement_projects_option_ref_by_borrow` (4fb8074) by-
14003 // borrow pins on the outer top-level [`Caixa`]
14004 // `Option<&Composite>`-return sub-family — extended here to
14005 // the fifth and final axis of the same sub-family, closing
14006 // the discipline: the accessor's returned reference must
14007 // borrow from `&self` (the returned reference's lifetime is
14008 // tied to `&self`), and calling the accessor twice on the
14009 // same [`Caixa`] must yield references that are pointer-equal
14010 // (the underlying byte-buffer is the storage `Entrada`'s
14011 // allocation, not a fresh copy) as well as value-equal
14012 // (idempotent, no side effects on `&self`).
14013 //
14014 // Pins against a future silent detour that returned an owned
14015 // `Entrada` (which would type-check via the `Clone` impl but
14016 // silently clone on every call), a `&Entrada` panic-return on
14017 // the `None` arm (which would collapse the load-bearing
14018 // `Option` presence-bit into a runtime panic), or a one-arm-
14019 // only accessor that returned a saturating composite on some
14020 // sentinel input.
14021 use crate::aplicacao::Entrada;
14022 for entrada in [
14023 Some(Entrada {
14024 host: "checkout.quero.cloud".into(),
14025 para: "gateway".into(),
14026 paths: Vec::new(),
14027 port: crate::DEFAULT_SERVICO_PORT,
14028 }),
14029 Some(Entrada {
14030 host: "api.pleme.io".into(),
14031 para: "public-api".into(),
14032 paths: vec!["/v1".into(), "/v2".into()],
14033 port: 8080,
14034 }),
14035 ] {
14036 let c = caixa_aplicacao_with_entrada(entrada.clone());
14037 let first = c.entrada().unwrap();
14038 let second = c.entrada().unwrap();
14039 assert_eq!(
14040 first, second,
14041 "Caixa::entrada must be idempotent — two successive \
14042 calls on the same &self must return the same &Entrada",
14043 );
14044 assert!(
14045 std::ptr::eq(first, second),
14046 "Caixa::entrada must borrow the underlying \
14047 Option<Entrada> storage — two successive calls must \
14048 return references with the same backing pointer (a \
14049 fresh Entrada clone would change the pointer on every \
14050 call)",
14051 );
14052 assert_eq!(
14053 Some(first),
14054 entrada.as_ref(),
14055 "Caixa::entrada must return :entrada verbatim by \
14056 borrow — got {first:?}, expected {:?}",
14057 entrada.as_ref(),
14058 );
14059 }
14060 let c = caixa_aplicacao_with_entrada(None);
14061 assert!(
14062 c.entrada().is_none(),
14063 "Caixa::entrada must return None when :entrada is absent \
14064 — the author-omitted arm must project through the \
14065 accessor's Option::None unchanged",
14066 );
14067 }
14068
14069 // ── Caixa::estrategia — outer top-level Option<RestartStrategy> flat-spread supervisor-tree accessor ──
14070
14071 fn caixa_with_estrategia(estrategia: Option<crate::supervisor::RestartStrategy>) -> Caixa {
14072 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
14073 c.estrategia = estrategia;
14074 c
14075 }
14076
14077 #[test]
14078 fn estrategia_returns_estrategia_option_verbatim_across_permutations() {
14079 // The canonical per-`Caixa` `:estrategia` M2 supervisor-tree-slot
14080 // flat-spread `Option<RestartStrategy>`-return `Copy`-composite-
14081 // enum-arm scalar shape pin: [`Caixa::estrategia`] must return
14082 // the `:estrategia` typed `Option<crate::supervisor::RestartStrategy>`
14083 // verbatim as an `Option<RestartStrategy>` `Copy`-projected value
14084 // over the same discriminant the raw `self.estrategia` field
14085 // access carries, byte-equal across every representative fixture
14086 // in the accept-set — the author-omitted `None` shape (the
14087 // "defer to [`RestartStrategy::default`] through the
14088 // [`Self::supervisor_view`] `unwrap_or_default()` fold" partition
14089 // every non-`Supervisor`-kind `defcaixa` carries by
14090 // `#[serde(default)]`), and each of the four closed-set variants
14091 // [`RestartStrategy::OneForOne`] / [`RestartStrategy::OneForAll`]
14092 // / [`RestartStrategy::RestForOne`] /
14093 // [`RestartStrategy::SimpleOneForOne`] the author-declared arm
14094 // partitions on.
14095 //
14096 // Pins against a future silent detour that re-derived the
14097 // strategy from a peer axis (an accidental fallback to
14098 // `if children.is_empty() { SimpleOneForOne } else { OneForOne }`
14099 // collapse that read the outer `:children` list-length axis into
14100 // the strategy discriminator at the accessor boundary), a
14101 // stale-derive detour that substituted [`RestartStrategy::default`]
14102 // when the outer `Option` held `None` (which would silently
14103 // collapse the load-bearing "author explicitly declared
14104 // `:estrategia OneForOne`" vs "author omitted the slot and
14105 // inherited the default" partition the [`Self::declared_supervisor_slots`]
14106 // presence-probe reads — the enumerator gate would still push
14107 // `SUPERVISOR_AUTHOR_KEY_ESTRATEGIA` on the omitted arm, silently
14108 // splitting the paired [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
14109 // kind-coherence gate's traversal head from the
14110 // [`Self::supervisor_view`] `unwrap_or_default()` fold's
14111 // composition head), a reference to an operator-resolved overlay
14112 // (the future per-cluster `:estrategia-overrides` slot — its
14113 // resolution must land at exactly this accessor body, not
14114 // silently divert the raw slot away from a second consumer), or
14115 // an axis-remap projection (a future detour that mapped
14116 // `OneForAll` through the accessor onto `OneForOne` would
14117 // silently split every downstream sibling-restart-strategy
14118 // consumer's per-arm fan-out).
14119 //
14120 // First outer top-level [`Caixa`] `Option<Copy>`-return
14121 // supervisor-tree-slot flat-spread accessor pin on the substrate
14122 // primitive — opens the outer-`Caixa` `Option<Copy>` flat-spread
14123 // projection pattern the sibling per-`Caixa` `:max-restarts` /
14124 // `:restart-window` future outer-scalar pins fold on. Peer of
14125 // the inner-altitude
14126 // `supervisor_spec_estrategia_returns_estrategia_verbatim_across_permutations`
14127 // (eafb619) pin on the post-composition [`SupervisorSpec`]
14128 // altitude — same "the substrate-primitive accessor must byte-
14129 // equal the raw field access verbatim across every author-
14130 // declared value" discipline extended onto the pre-composition
14131 // outer author-surface [`Caixa`] altitude. Peer of the closed
14132 // outer-`Caixa` `Option<&Composite>` composite-reference family
14133 // the sibling `limits` / `behavior` / `politicas` / `placement` /
14134 // `entrada`
14135 // `..._returns_..._option_ref_verbatim_across_permutations` pins
14136 // already carry on the outer `Option<&Composite>` altitude.
14137 use crate::supervisor::RestartStrategy;
14138 let fixtures: Vec<Option<RestartStrategy>> = vec![
14139 None,
14140 Some(RestartStrategy::OneForOne),
14141 Some(RestartStrategy::OneForAll),
14142 Some(RestartStrategy::RestForOne),
14143 Some(RestartStrategy::SimpleOneForOne),
14144 ];
14145 for estrategia in fixtures {
14146 let c = caixa_with_estrategia(estrategia);
14147 assert_eq!(
14148 c.estrategia(),
14149 estrategia,
14150 "Caixa::estrategia must return :estrategia verbatim (got \
14151 {:?}, expected {:?})",
14152 c.estrategia(),
14153 estrategia,
14154 );
14155 assert_eq!(
14156 c.estrategia(),
14157 c.estrategia,
14158 "Caixa::estrategia accessor and self.estrategia field \
14159 access must byte-equal — the accessor is the substrate-\
14160 primitive typed dispatch every downstream supervisor-\
14161 tree flat-spread consumer must route through, and a \
14162 discriminant split would silently break every consumer \
14163 that relied on the accessor sharing the field's own \
14164 Option<Copy> shape",
14165 );
14166 assert_eq!(
14167 c.estrategia().is_some(),
14168 c.estrategia.is_some(),
14169 "Caixa::estrategia().is_some() must byte-equal \
14170 self.estrategia.is_some() — a presence-bit drift would \
14171 silently split the paired Caixa::declared_supervisor_slots \
14172 presence-probe arm from the Caixa::supervisor_view \
14173 unwrap_or_default() fold's composition input",
14174 );
14175 }
14176 }
14177
14178 #[test]
14179 fn declared_supervisor_slots_estrategia_arm_routes_through_accessor() {
14180 // Composition pin: [`Caixa::declared_supervisor_slots`]'s
14181 // `:estrategia` presence-probe arm must key off
14182 // [`Caixa::estrategia`], not the raw `self.estrategia.is_some()`
14183 // field-probe. Structurally: every `Caixa { estrategia:
14184 // Some(RestartStrategy::_), .. }` variant must push
14185 // `SUPERVISOR_AUTHOR_KEY_ESTRATEGIA` onto the declared-slot list
14186 // (the presence bit is `Some` for every closed-set variant, so
14187 // the M2 supervisor-tree kind-coherence gate must surface the
14188 // slot as "declared" regardless of which variant the author
14189 // picked), and a `Caixa { estrategia: None, .. }` must NOT push
14190 // the label (the "author omitted the slot entirely, deferring
14191 // to [`RestartStrategy::default`] through the supervisor_view
14192 // fold" partition). The pair jointly pins the accessor +
14193 // declared-slot enumerator composition: any future silent detour
14194 // that had the accessor collapse `Some(RestartStrategy::default())`
14195 // to `None` (a `.filter(|e| *e != RestartStrategy::default())`
14196 // projection) would silently absorb the "declared but default-
14197 // valued" arm at the accessor boundary and the
14198 // [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] kind-
14199 // coherence gate would silently accept a struct-literal `Caixa`
14200 // carrying the drift.
14201 //
14202 // Peer of the sibling per-`Caixa`
14203 // `declared_servico_slots_limits_arm_routes_through_accessor`
14204 // (b2bd9d7) accessor-composition pin on the sibling outer-`Caixa`
14205 // `Option<&LimitsSpec>` composition axis — same "the enumerator
14206 // gate must route through the substrate-primitive typed
14207 // dispatch" discipline extended onto the flat-spread M2
14208 // supervisor-tree `Option<RestartStrategy>`-composition surface,
14209 // opening the outer-`Caixa` supervisor-tree-slot arm of the
14210 // composition-pin family.
14211 use crate::supervisor::RestartStrategy;
14212 for estrategia in [
14213 RestartStrategy::OneForOne,
14214 RestartStrategy::OneForAll,
14215 RestartStrategy::RestForOne,
14216 RestartStrategy::SimpleOneForOne,
14217 ] {
14218 let c = caixa_with_estrategia(Some(estrategia));
14219 let slots = c.declared_supervisor_slots();
14220 assert!(
14221 slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA),
14222 "declared_supervisor_slots must push \
14223 SUPERVISOR_AUTHOR_KEY_ESTRATEGIA when `:estrategia` is \
14224 Some({estrategia:?}) — the accessor and the enumerator \
14225 gate must route through the same substrate-primitive \
14226 typed dispatch on the outer :estrategia presence bit \
14227 (got slots={slots:?})",
14228 );
14229 }
14230 let c = caixa_with_estrategia(None);
14231 let slots = c.declared_supervisor_slots();
14232 assert!(
14233 !slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA),
14234 "declared_supervisor_slots must NOT push \
14235 SUPERVISOR_AUTHOR_KEY_ESTRATEGIA when `:estrategia` is None \
14236 — the author-omitted arm must route through the accessor's \
14237 None-return unchanged (got slots={slots:?})",
14238 );
14239 }
14240
14241 #[test]
14242 fn supervisor_view_estrategia_arm_routes_through_accessor() {
14243 // Composition pin: [`Caixa::supervisor_view`]'s per-`:estrategia`
14244 // [`SupervisorSpec`] construction arm must key off
14245 // [`Caixa::estrategia`]'s `unwrap_or_default()` fold, not the raw
14246 // `self.estrategia.unwrap_or_default()` field-fold. Structurally:
14247 // for every `:kind Supervisor` `Caixa` carrying an author-
14248 // declared `Some(RestartStrategy::_)` variant, the composed
14249 // [`SupervisorSpec`]'s `.estrategia` field must byte-equal the
14250 // outer accessor's declared variant unchanged; and for a
14251 // `:kind Supervisor` `Caixa` carrying `None`, the composed
14252 // [`SupervisorSpec`]'s `.estrategia` field must byte-equal
14253 // [`RestartStrategy::default`] (the [`RestartStrategy::OneForOne`]
14254 // arm the flat-spread `unwrap_or_default()` fold projects to on
14255 // the author-omitted arm — this is the *composition* between the
14256 // outer `Option<RestartStrategy>` accessor's presence-bit
14257 // surface and the inner post-composition non-`Option`
14258 // [`SupervisorSpec::estrategia`] altitude). The pair jointly
14259 // pins the accessor + supervisor_view composition: any future
14260 // silent detour that had the accessor promote `None` to
14261 // `Some(RestartStrategy::default())` (a `.or_else(|| Some(RestartStrategy::default()))`
14262 // projection) would silently collapse the two arms into one at
14263 // the accessor boundary and the [`Self::declared_supervisor_slots`]
14264 // presence probe would silently drift from the composition site.
14265 //
14266 // Peer of the sibling M2 supervisor-slot post-composition
14267 // `validate_reads_through_lifted_estrategia_accessor` (eafb619)
14268 // pin on the [`SupervisorSpec::validate`] altitude — this pin
14269 // extends that inner-altitude accessor-routing discipline onto
14270 // the pre-composition outer author-surface [`Caixa`] altitude,
14271 // pinning the composition edge between the flat-spread outer
14272 // `Option<RestartStrategy>` and the composed [`SupervisorSpec`]
14273 // `RestartStrategy` axes.
14274 use crate::CaixaKind;
14275 use crate::supervisor::{ChildSpec, RestartPolicy, RestartStrategy};
14276 for estrategia in [
14277 RestartStrategy::OneForOne,
14278 RestartStrategy::OneForAll,
14279 RestartStrategy::RestForOne,
14280 RestartStrategy::SimpleOneForOne,
14281 ] {
14282 let mut c = caixa_with_estrategia(Some(estrategia));
14283 c.kind = CaixaKind::Supervisor;
14284 // Route the `SimpleOneForOne ↔ non-SimpleOneForOne` fixture-
14285 // shape partition through the [`gen_platform::IsVariant`]
14286 // derive-generated
14287 // [`RestartStrategy::is_simple_one_for_one`] predicate rather
14288 // than the raw `matches!(estrategia, RestartStrategy::
14289 // SimpleOneForOne)` open-coded pattern-match — same closed-
14290 // set-typed-enum arm-discriminator dispatch discipline the
14291 // sibling [`crate::upgrade::UpgradeInstruction::is_restart`]
14292 // convergence (915a934) extended onto its two paired positive
14293 // / negated `matches!` sites and the peer
14294 // [`crate::aplicacao::PlacementStrategy`] `IsVariant`-derived
14295 // predicate convergence (766ec63) extended onto the M3 mesh-
14296 // slot per-`:placement` distribution-strategy discriminator
14297 // axis. See the sibling `supervisor::tests::
14298 // round_trip_all_strategies` and
14299 // `supervisor::tests::supervisor_spec_estrategia_returns_estrategia_verbatim_across_permutations`
14300 // fixtures — the three sites (all test-only,
14301 // acknowledged in 915a934's Prior-commits footnote as the
14302 // outstanding follow-up) now consult one typed dispatch on
14303 // the substrate primitive.
14304 c.children = if estrategia.is_simple_one_for_one() {
14305 Vec::new()
14306 } else {
14307 vec![ChildSpec {
14308 caixa: "worker".into(),
14309 versao: "^0.1".into(),
14310 restart: RestartPolicy::Permanent,
14311 }]
14312 };
14313 let view = c.supervisor_view().expect(
14314 "supervisor_view must materialize a SupervisorSpec for a \
14315 :kind Supervisor Caixa carrying a Some(:estrategia) slot",
14316 );
14317 assert_eq!(
14318 view.estrategia(),
14319 c.estrategia().unwrap(),
14320 "supervisor_view must carry the outer Caixa::estrategia() \
14321 declared variant onto the composed SupervisorSpec.estrategia \
14322 field verbatim on the Some arm (got {:?}, expected {:?})",
14323 view.estrategia(),
14324 c.estrategia().unwrap(),
14325 );
14326 }
14327 // The author-omitted arm: outer `None` → composed
14328 // `RestartStrategy::default()` through the flat-spread
14329 // `unwrap_or_default()` fold.
14330 let mut c = caixa_with_estrategia(None);
14331 c.kind = CaixaKind::Supervisor;
14332 // Populate children so the sibling supervisor slots are coherent
14333 // for the [`Self::supervisor_view`] projection; the `:estrategia`
14334 // arm still defers to [`RestartStrategy::default`] on the
14335 // author-omitted arm even when the sibling slots carry values.
14336 c.children = vec![ChildSpec {
14337 caixa: "worker".into(),
14338 versao: "^0.1".into(),
14339 restart: RestartPolicy::Permanent,
14340 }];
14341 let view = c.supervisor_view().expect(
14342 "supervisor_view must materialize a SupervisorSpec for a \
14343 :kind Supervisor Caixa carrying a None `:estrategia` slot",
14344 );
14345 assert_eq!(
14346 view.estrategia(),
14347 RestartStrategy::default(),
14348 "supervisor_view must project the outer Caixa::estrategia() \
14349 None arm onto RestartStrategy::default() through the flat-\
14350 spread unwrap_or_default() fold (got {:?}, expected {:?})",
14351 view.estrategia(),
14352 RestartStrategy::default(),
14353 );
14354 assert!(
14355 c.estrategia().is_none(),
14356 "Caixa::estrategia() must remain None on the author-omitted \
14357 arm — the supervisor_view fold must not mutate the outer \
14358 flat-spread presence bit",
14359 );
14360 }
14361
14362 #[test]
14363 fn estrategia_projects_option_by_copy() {
14364 // The by-`Copy` pin: [`Caixa::estrategia`] returns
14365 // `Option<RestartStrategy>` by value (`RestartStrategy: Copy`) —
14366 // the accessor does not borrow `&self` past the call (no
14367 // lifetime on the return type), and calling the accessor twice
14368 // on the same [`Caixa`] must yield discriminant-equal values
14369 // (idempotent, no side effects on `&self`). Peer of the sibling
14370 // outer-`Caixa` `Option<&Composite>` by-borrow
14371 // `limits_projects_option_ref_by_borrow` (b2bd9d7) /
14372 // `behavior_projects_option_ref_by_borrow` (35d8b52) /
14373 // `politicas_projects_option_ref_by_borrow` (5d23d29) /
14374 // `placement_projects_option_ref_by_borrow` (4fb8074) /
14375 // `entrada_projects_option_ref_by_borrow` (e4128e4) by-borrow
14376 // pins on the outer-`Caixa` `Option<&Composite>`-return axes —
14377 // extended here to the outer-`Caixa` `Option<Copy>`-return
14378 // flat-spread axis. The `Copy` discipline replaces the pointer-
14379 // equality claim the by-borrow siblings pin (a fresh `Copy` of a
14380 // `Copy` discriminant is definitionally the same discriminant, so
14381 // the axis reduces to discriminant equality).
14382 //
14383 // Pins against a future silent detour that returned a fresh
14384 // `Option<&RestartStrategy>` (which would type-check but silently
14385 // introduce a borrow of `&self` past the call, collapsing the
14386 // load-bearing "no lifetime on the return type" `Copy` projection
14387 // the flat-spread axis's `Option<Copy>` shape carries), a stale-
14388 // read side effect that flipped the outer discriminant on
14389 // successive calls, or an axis-remap projection that returned a
14390 // different variant than the field storage.
14391 use crate::supervisor::RestartStrategy;
14392 for estrategia in [
14393 Some(RestartStrategy::OneForOne),
14394 Some(RestartStrategy::OneForAll),
14395 Some(RestartStrategy::RestForOne),
14396 Some(RestartStrategy::SimpleOneForOne),
14397 ] {
14398 let c = caixa_with_estrategia(estrategia);
14399 let first = c.estrategia();
14400 let second = c.estrategia();
14401 assert_eq!(
14402 first, second,
14403 "Caixa::estrategia must be idempotent — two successive \
14404 calls on the same &self must return the same \
14405 Option<RestartStrategy>",
14406 );
14407 assert_eq!(
14408 first, estrategia,
14409 "Caixa::estrategia must return :estrategia verbatim by \
14410 Copy — got {first:?}, expected {estrategia:?}",
14411 );
14412 }
14413 let c = caixa_with_estrategia(None);
14414 assert!(
14415 c.estrategia().is_none(),
14416 "Caixa::estrategia must return None when :estrategia is \
14417 absent — the author-omitted arm must project through the \
14418 accessor's Option::None unchanged",
14419 );
14420 }
14421
14422 // ── Caixa::max_restarts / Caixa::restart_window —
14423 // outer top-level M2 supervisor-tree-slot flat-spread accessors
14424 // (Option<u32> / Option<&str>) folding on the ed04d3c
14425 // Caixa::estrategia Option<Copy> sub-family ─────────────────────
14426
14427 fn caixa_with_max_restarts(max_restarts: Option<u32>) -> Caixa {
14428 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
14429 c.max_restarts = max_restarts;
14430 c
14431 }
14432
14433 fn caixa_supervisor_with_max_restarts_and_window(
14434 max_restarts: Option<u32>,
14435 restart_window: Option<&str>,
14436 ) -> Caixa {
14437 use crate::CaixaKind;
14438 use crate::supervisor::{ChildSpec, RestartPolicy};
14439 let mut c = Caixa::from_lisp(&Caixa::template("root")).unwrap();
14440 c.kind = CaixaKind::Supervisor;
14441 c.max_restarts = max_restarts;
14442 c.restart_window = restart_window.map(str::to_string);
14443 c.children = vec![ChildSpec {
14444 caixa: "worker".into(),
14445 versao: "^0.1".into(),
14446 restart: RestartPolicy::Permanent,
14447 }];
14448 c
14449 }
14450
14451 #[test]
14452 fn max_restarts_returns_max_restarts_option_verbatim_across_permutations() {
14453 // Value-shape pin: [`Caixa::max_restarts`] returns the
14454 // `:max-restarts` typed `Option<u32>` verbatim, `Copy`-projected
14455 // from the typed slot's own storage, byte-equal across the
14456 // author-omitted `None` arm (the "defer to the
14457 // [`Self::supervisor_view`] `unwrap_or(5)` OTP-canonical
14458 // `{intensity, 5, 60}` default" partition every
14459 // non-`Supervisor`-kind caixa carries by `#[serde(default)]`)
14460 // and each of the representative fixtures in the accept-set —
14461 // `0` (the zero-floor arm the peer
14462 // [`crate::supervisor::SupervisorSpec::validate`]
14463 // [`crate::SupervisorError::ZeroMaxRestarts`] gate refuses on
14464 // the post-composition altitude — the accessor must ship the
14465 // raw slot verbatim so struct-literal fixtures continue to
14466 // expose the zero at the accessor boundary), the OTP-canonical
14467 // `5` default (`{intensity, 5, 60}` worker-supervisor from
14468 // Learn You Some Erlang), `1000` (the
14469 // [`SUPERVISOR_MAX_RESTARTS_MAX`] cap the peer post-composition
14470 // upper-bound gate accepts on the boundary), `u32::MAX` (a
14471 // past-the-cap sentinel that the substrate-primitive accessor
14472 // must still ship verbatim). Second outer top-level
14473 // [`Caixa`] `Option<Copy>`-return supervisor-tree flat-spread
14474 // pin — folds on the sibling
14475 // `estrategia_returns_estrategia_option_verbatim_across_permutations`
14476 // (ed04d3c) pin's `Option<Copy>` shape, extending the sub-family
14477 // onto the sibling `Option<u32>` restart-budget-count arm.
14478 let fixtures: Vec<Option<u32>> = vec![None, Some(0), Some(5), Some(1000), Some(u32::MAX)];
14479 for max_restarts in fixtures {
14480 let c = caixa_with_max_restarts(max_restarts);
14481 assert_eq!(
14482 c.max_restarts(),
14483 max_restarts,
14484 "Caixa::max_restarts must return :max-restarts verbatim \
14485 (got {:?}, expected {max_restarts:?})",
14486 c.max_restarts(),
14487 );
14488 assert_eq!(
14489 c.max_restarts(),
14490 c.max_restarts,
14491 "Caixa::max_restarts accessor and self.max_restarts \
14492 field access must byte-equal — a presence-bit or count \
14493 drift would silently split the paired \
14494 Caixa::declared_supervisor_slots presence-probe arm \
14495 from the Caixa::supervisor_view unwrap_or(5) fold's \
14496 composition input",
14497 );
14498 }
14499 }
14500
14501 #[test]
14502 fn max_restarts_projects_option_by_copy() {
14503 // The by-`Copy` pin: [`Caixa::max_restarts`] returns
14504 // `Option<u32>` by value (`u32: Copy`) — the accessor does not
14505 // borrow `&self` past the call (no lifetime on the return type),
14506 // and calling the accessor twice on the same [`Caixa`] must
14507 // yield equal values (idempotent, no side effects). Peer of the
14508 // sibling `estrategia_projects_option_by_copy` (ed04d3c) pin on
14509 // the outer-`Caixa` `Option<Copy>`-return flat-spread axis.
14510 for max_restarts in [Some(0u32), Some(5), Some(1000), Some(u32::MAX), None] {
14511 let c = caixa_with_max_restarts(max_restarts);
14512 let first = c.max_restarts();
14513 let second = c.max_restarts();
14514 assert_eq!(
14515 first, second,
14516 "Caixa::max_restarts must be idempotent — two successive \
14517 calls on the same &self must return the same Option<u32>",
14518 );
14519 assert_eq!(
14520 first, max_restarts,
14521 "Caixa::max_restarts must return :max-restarts verbatim \
14522 by Copy — got {first:?}, expected {max_restarts:?}",
14523 );
14524 }
14525 }
14526
14527 #[test]
14528 fn declared_supervisor_slots_max_restarts_arm_routes_through_accessor() {
14529 // Composition pin: [`Caixa::declared_supervisor_slots`]'s
14530 // `:max-restarts` presence-probe arm must key off
14531 // [`Caixa::max_restarts`], not the raw
14532 // `self.max_restarts.is_some()` field-probe. Structurally: every
14533 // `Caixa { max_restarts: Some(_), .. }` variant must push
14534 // `SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS` onto the declared-slot
14535 // list (the presence bit is `Some` for every representative
14536 // count, so the M2 kind-coherence gate must surface the slot as
14537 // "declared"), and a `Caixa { max_restarts: None, .. }` must
14538 // NOT push the label. Peer of the sibling
14539 // `declared_supervisor_slots_estrategia_arm_routes_through_accessor`
14540 // (ed04d3c) composition pin — same routing-through-accessor
14541 // discipline extended onto the sibling flat-spread `Option<u32>`
14542 // arm.
14543 for max_restarts in [0u32, 5, 1000, u32::MAX] {
14544 let c = caixa_with_max_restarts(Some(max_restarts));
14545 let slots = c.declared_supervisor_slots();
14546 assert!(
14547 slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS),
14548 "declared_supervisor_slots must push \
14549 SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS when `:max-restarts` \
14550 is Some({max_restarts}) — the accessor and the \
14551 enumerator gate must route through the same \
14552 substrate-primitive typed dispatch on the outer \
14553 :max-restarts presence bit (got slots={slots:?})",
14554 );
14555 }
14556 let c = caixa_with_max_restarts(None);
14557 let slots = c.declared_supervisor_slots();
14558 assert!(
14559 !slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS),
14560 "declared_supervisor_slots must NOT push \
14561 SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS when `:max-restarts` is \
14562 None — the author-omitted arm must route through the \
14563 accessor's None-return unchanged (got slots={slots:?})",
14564 );
14565 }
14566
14567 #[test]
14568 fn supervisor_view_max_restarts_arm_routes_through_accessor() {
14569 // Composition pin: [`Caixa::supervisor_view`]'s per-`:max-restarts`
14570 // [`SupervisorSpec`] construction arm must key off
14571 // [`Caixa::max_restarts`]'s `unwrap_or(5)` fold, not the raw
14572 // `self.max_restarts.unwrap_or(5)` field-fold. Structurally: for
14573 // every `:kind Supervisor` `Caixa` carrying an author-declared
14574 // `Some(n)`, the composed [`SupervisorSpec`]'s `.max_restarts()`
14575 // must byte-equal `n`; and for a `:kind Supervisor` `Caixa`
14576 // carrying `None`, the composed [`SupervisorSpec`]'s
14577 // `.max_restarts()` must byte-equal the OTP-canonical `5`. Peer
14578 // of the sibling
14579 // `supervisor_view_estrategia_arm_routes_through_accessor`
14580 // (ed04d3c) composition pin.
14581 for max_restarts in [1u32, 5, 1000] {
14582 let c = caixa_supervisor_with_max_restarts_and_window(Some(max_restarts), None);
14583 let view = c.supervisor_view().expect(
14584 "supervisor_view must materialize a SupervisorSpec for a \
14585 :kind Supervisor Caixa carrying a Some(:max-restarts)",
14586 );
14587 assert_eq!(
14588 view.max_restarts(),
14589 max_restarts,
14590 "supervisor_view must carry the outer \
14591 Caixa::max_restarts() Some arm onto the composed \
14592 SupervisorSpec.max_restarts field verbatim (got {}, \
14593 expected {max_restarts})",
14594 view.max_restarts(),
14595 );
14596 }
14597 let c = caixa_supervisor_with_max_restarts_and_window(None, None);
14598 let view = c.supervisor_view().expect(
14599 "supervisor_view must materialize a SupervisorSpec for a \
14600 :kind Supervisor Caixa carrying a None :max-restarts",
14601 );
14602 assert_eq!(
14603 view.max_restarts(),
14604 5,
14605 "supervisor_view must project the outer \
14606 Caixa::max_restarts() None arm onto the OTP-canonical \
14607 {{intensity, 5, 60}} default (5) through the flat-spread \
14608 unwrap_or(5) fold (got {})",
14609 view.max_restarts(),
14610 );
14611 assert!(
14612 c.max_restarts().is_none(),
14613 "Caixa::max_restarts() must remain None on the author-\
14614 omitted arm — the supervisor_view fold must not mutate \
14615 the outer flat-spread presence bit",
14616 );
14617 }
14618
14619 #[test]
14620 fn restart_window_returns_restart_window_option_verbatim_across_permutations() {
14621 // Value-shape pin: [`Caixa::restart_window`] returns the
14622 // `:restart-window` typed `Option<String>` verbatim as an
14623 // `Option<&str>`, borrowed from the typed slot's own storage,
14624 // byte-equal across the author-omitted `None` arm and each of
14625 // the representative fixtures in the accept-set — the canonical
14626 // `"60s"` from `{intensity, 5, 60}`, the sibling
14627 // canonical-magnitude forms (`"5m"` / `"1h"` / `"500ms"` / `"30"`
14628 // / `"0s"`) the shared codec's positive-set sweep pin covers,
14629 // plus a past-the-guard sentinel (`"1.5s"` — the fractional-
14630 // seconds drift the sibling [`Self::validate_restart_window`]
14631 // gate refuses; the accessor must ship the raw slot verbatim
14632 // so struct-literal fixtures continue to expose the drift at
14633 // the accessor boundary). Third outer top-level [`Caixa`]
14634 // supervisor-tree flat-spread pin — extends the sub-family onto
14635 // the sibling `Option<&str>` raw-duration-string arm.
14636 for window in [
14637 None,
14638 Some("60s"),
14639 Some("5m"),
14640 Some("1h"),
14641 Some("500ms"),
14642 Some("1.5s"),
14643 Some(""),
14644 ] {
14645 let c = caixa_with_restart_window(window);
14646 assert_eq!(
14647 c.restart_window(),
14648 window,
14649 "Caixa::restart_window must return :restart-window \
14650 verbatim as Option<&str> (got {:?}, expected {window:?})",
14651 c.restart_window(),
14652 );
14653 assert_eq!(
14654 c.restart_window(),
14655 c.restart_window.as_deref(),
14656 "Caixa::restart_window accessor and \
14657 self.restart_window.as_deref() field access must \
14658 byte-equal — a byte-level drift would silently split \
14659 the paired Caixa::declared_supervisor_slots \
14660 presence-probe arm from the \
14661 Caixa::validate_restart_window shared-codec gate and \
14662 the Caixa::supervisor_view soft-swallowing fold",
14663 );
14664 }
14665 }
14666
14667 #[test]
14668 fn restart_window_projects_slice_by_borrow() {
14669 // The by-borrow pin: [`Caixa::restart_window`] returns
14670 // `Option<&str>` by borrow — the returned string slice borrows
14671 // the underlying `Option<String>` storage of the `:restart-window`
14672 // slot and the accessor must not clone on every call. Peer of
14673 // the sibling outer top-level [`Caixa`] `Option<&str>`-return
14674 // by-borrow pins on the universal-axis scalar family
14675 // (`licenca_projects_option_ref_by_borrow` /
14676 // `descricao_projects_option_ref_by_borrow` and siblings) —
14677 // extended onto the M2 supervisor-tree flat-spread
14678 // `Option<&str>` raw-duration-string axis.
14679 for window in [None, Some("60s"), Some("5m"), Some("")] {
14680 let c = caixa_with_restart_window(window);
14681 let first = c.restart_window();
14682 let second = c.restart_window();
14683 assert_eq!(
14684 first, second,
14685 "Caixa::restart_window must be idempotent — two \
14686 successive calls on the same &self must return the \
14687 same Option<&str>",
14688 );
14689 if let (Some(a), Some(b)) = (first, second) {
14690 assert_eq!(
14691 a.as_ptr(),
14692 b.as_ptr(),
14693 "Caixa::restart_window must borrow the underlying \
14694 String storage — two successive Some-arm calls must \
14695 return slices with the same backing pointer (a fresh \
14696 String clone would change the pointer on every call)",
14697 );
14698 }
14699 assert_eq!(
14700 first, window,
14701 "Caixa::restart_window must return :restart-window \
14702 verbatim by borrow — got {first:?}, expected {window:?}",
14703 );
14704 }
14705 }
14706
14707 #[test]
14708 fn declared_supervisor_slots_restart_window_arm_routes_through_accessor() {
14709 // Composition pin: [`Caixa::declared_supervisor_slots`]'s
14710 // `:restart-window` presence-probe arm must key off
14711 // [`Caixa::restart_window`], not the raw
14712 // `self.restart_window.is_some()` field-probe. Structurally:
14713 // every `Caixa { restart_window: Some(_), .. }` must push
14714 // `SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW` onto the declared-slot
14715 // list, and a `Caixa { restart_window: None, .. }` must NOT
14716 // push the label. Peer of the sibling
14717 // `declared_supervisor_slots_max_restarts_arm_routes_through_accessor`
14718 // routing pin.
14719 for window in ["60s", "5m", "1h", "500ms", "1.5s", ""] {
14720 let c = caixa_with_restart_window(Some(window));
14721 let slots = c.declared_supervisor_slots();
14722 assert!(
14723 slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW),
14724 "declared_supervisor_slots must push \
14725 SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW when \
14726 `:restart-window` is Some({window:?}) — the accessor \
14727 and the enumerator gate must route through the same \
14728 substrate-primitive typed dispatch on the outer \
14729 :restart-window presence bit (got slots={slots:?})",
14730 );
14731 }
14732 let c = caixa_with_restart_window(None);
14733 let slots = c.declared_supervisor_slots();
14734 assert!(
14735 !slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW),
14736 "declared_supervisor_slots must NOT push \
14737 SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW when `:restart-window` \
14738 is None — the author-omitted arm must route through the \
14739 accessor's None-return unchanged (got slots={slots:?})",
14740 );
14741 }
14742
14743 #[test]
14744 fn validate_restart_window_arm_routes_through_accessor() {
14745 // Composition pin: [`Caixa::validate_restart_window`]'s
14746 // shared-codec fold arm must key off [`Caixa::restart_window`],
14747 // not the raw `self.restart_window.as_deref()` field-projection.
14748 // Structurally: (1) `None` → `Ok(())` (the "omit the slot to
14749 // express no reset" canonical shape); (2) a canonical `Some`
14750 // arm (`"60s"`) → `Ok(())`; (3) a codec-rejected `Some` arm
14751 // (`"1.5s"`) → `Err(RestartWindowMalformed { restart_window,
14752 // .. })` carrying the offending raw string verbatim. The three
14753 // arms jointly pin that the validator's raw-string binding is
14754 // the accessor's return, not a peer projection — any future
14755 // silent detour that had the accessor collapse `Some("")` to
14756 // `None` would silently absorb the empty-after-trim refusal
14757 // case at the accessor boundary.
14758 caixa_with_restart_window(None)
14759 .validate_restart_window()
14760 .expect("None :restart-window must validate through the accessor");
14761 caixa_with_restart_window(Some("60s"))
14762 .validate_restart_window()
14763 .expect("canonical :restart-window \"60s\" must validate through the accessor");
14764 let err = caixa_with_restart_window(Some("1.5s"))
14765 .validate_restart_window()
14766 .expect_err("fractional-seconds :restart-window must fail through the accessor");
14767 assert!(
14768 matches!(
14769 err,
14770 ManifestError::RestartWindowMalformed { ref restart_window, .. }
14771 if restart_window == "1.5s"
14772 ),
14773 "validator must carry the offending raw string verbatim \
14774 from the accessor's borrowed &str (got {err:?})",
14775 );
14776 }
14777
14778 #[test]
14779 fn supervisor_view_restart_window_arm_routes_through_accessor() {
14780 // Composition pin: [`Caixa::supervisor_view`]'s
14781 // per-`:restart-window` [`SupervisorSpec`] construction arm
14782 // must key off [`Caixa::restart_window`]'s soft-swallowing
14783 // `.and_then(|s| duration_codec::parse(s).ok())` fold, not the
14784 // raw `self.restart_window.as_deref().and_then(…)` field-fold.
14785 // Structurally: (1) `None` → `SupervisorSpec.restart_window ==
14786 // None` (the "never reset" sentinel); (2) canonical `Some("60s")`
14787 // → `SupervisorSpec.restart_window == Some(Duration::from_secs(60))`
14788 // (the shared codec's canonical parse); (3) codec-rejected
14789 // `Some("1.5s")` → `SupervisorSpec.restart_window == None`
14790 // (the soft-swallow preserving the view's best-effort shape).
14791 let c = caixa_supervisor_with_max_restarts_and_window(None, None);
14792 let view = c.supervisor_view().expect("Supervisor kind has a view");
14793 assert_eq!(
14794 view.restart_window(),
14795 None,
14796 "supervisor_view must project outer None :restart-window \
14797 onto None on the composed SupervisorSpec (never-reset \
14798 sentinel) through the accessor's None-return unchanged",
14799 );
14800
14801 let c = caixa_supervisor_with_max_restarts_and_window(None, Some("60s"));
14802 let view = c.supervisor_view().expect("Supervisor kind has a view");
14803 assert_eq!(
14804 view.restart_window(),
14805 Some(std::time::Duration::from_secs(60)),
14806 "supervisor_view must fold outer Some(\"60s\") through the \
14807 shared duration_codec into Duration::from_secs(60) on the \
14808 composed SupervisorSpec (accessor's Some(&str) → codec \
14809 parse → Some(Duration))",
14810 );
14811
14812 let c = caixa_supervisor_with_max_restarts_and_window(None, Some("1.5s"));
14813 let view = c.supervisor_view().expect("Supervisor kind has a view");
14814 assert_eq!(
14815 view.restart_window(),
14816 None,
14817 "supervisor_view must soft-swallow the shared-codec parse \
14818 failure to None (the view's best-effort shape the sibling \
14819 manifest-level validate_restart_window surfaces as \
14820 RestartWindowMalformed); the accessor's raw-string return \
14821 is the single input every downstream consumer keys off",
14822 );
14823 }
14824
14825 // ── Caixa::upgrade_from — outer top-level &[UpgradeFromEntry] composite-slice accessor ──
14826
14827 fn caixa_with_upgrade_from(upgrade_from: Vec<crate::upgrade::UpgradeFromEntry>) -> Caixa {
14828 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
14829 c.upgrade_from = upgrade_from;
14830 c
14831 }
14832
14833 #[test]
14834 fn upgrade_from_returns_upgrade_from_slice_verbatim_across_permutations() {
14835 // The canonical per-`Caixa` `:upgrade-from` M2 typed-slot
14836 // outer-composite `&[UpgradeFromEntry]`-return slice-shape
14837 // pin: [`Caixa::upgrade_from`] must return the `:upgrade-from`
14838 // typed `Vec<UpgradeFromEntry>` verbatim as a
14839 // `&[UpgradeFromEntry]` slice-view over the same backing
14840 // buffer the raw `self.upgrade_from.as_slice()` field access
14841 // borrows from, element-equal across every representative
14842 // fixture in the accept-set — `[]` (the "no hot-upgrade path
14843 // declared" arm every `defcaixa` without an `:upgrade-from`
14844 // block carries; `#[serde(default)]` folds an omitted slot
14845 // onto `Vec::new()`), a canonical single-entry `Restart`
14846 // fixture (the shape most Servicos carry — a single prior
14847 // version with the fallback strategy), a canonical multi-
14848 // entry list carrying every typed instruction variant
14849 // (`LoadModule` / `StateChange` / `SoftPurge` / `Purge` /
14850 // `Restart`), and a past-the-guard sentinel — a duplicate-
14851 // `:from` `[(0.1.0, Restart), (0.1.0, Restart)]` entry pair
14852 // ([`crate::upgrade::validate_upgrade_from`] rejects through
14853 // `DuplicateFrom { from: "0.1.0" }` but the accessor must
14854 // ship the raw slot verbatim so struct-literal fixtures
14855 // continue to expose the duplicate at the accessor boundary).
14856 //
14857 // Pins against a future silent detour that returned an owned
14858 // `Vec<UpgradeFromEntry>` (which would type-check but silently
14859 // clone on every accessor call, breaking the zero-cost
14860 // projection every peer sibling slice accessor carries), a
14861 // `[dup, dup] → [dup]` dedup collapse (which would silently
14862 // absorb the `DuplicateFrom` refusal case at the accessor
14863 // boundary and the [`crate::StandardLayout::verify`] cross-
14864 // entry gate would silently accept a struct-literal `Caixa`
14865 // carrying the drift), a reference to an operator-resolved
14866 // overlay (the future per-cluster `:upgrade-overrides` slot
14867 // — its resolution must land at exactly this accessor body,
14868 // not silently divert the raw slot away from a second
14869 // consumer), or an axis-shuffled projection (a future detour
14870 // that reordered entries through the accessor would silently
14871 // split the paired [`crate::StandardLayout::verify`] per-
14872 // `:upgrade-from` shape gate's traversal input from the peer
14873 // [`crate::render::servico_m2_overlay`] emitter's projection
14874 // input, since the operator's hot-upgrade dispatch matches
14875 // per-`:from` and axis reordering would silently split the
14876 // per-entry script-path existence probe's iteration order
14877 // from the M2 overlay emitter's serialized-entry order).
14878 //
14879 // First outer top-level [`Caixa`] `&[Composite]`-return
14880 // slice accessor pin on the substrate primitive for M2 / M3
14881 // typed-slot vec-carry axes — opens the outer-`Caixa`
14882 // `&[Composite]` composite-slice projection pattern the
14883 // sibling `:children` [`crate::supervisor::ChildSpec`] /
14884 // `:membros` [`crate::aplicacao::Membro`] / `:contratos`
14885 // [`crate::aplicacao::WitContract`] future outer-composite-
14886 // slice pins fold on. Peer of the closed outer-`Caixa`
14887 // scalar `Option<&Composite>` composite-reference family the
14888 // sibling `limits` / `behavior` / `politicas` / `placement`
14889 // / `entrada` `..._returns_..._option_ref_verbatim_across_
14890 // permutations` pins closed (b2bd9d7 → e4128e4) — extends
14891 // the "byte-equal, borrow-shared" outer-accessor discipline
14892 // onto the outer-`Caixa` `&[Composite]` vec-carry altitude.
14893 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
14894 let fixtures: Vec<Vec<UpgradeFromEntry>> = vec![
14895 vec![],
14896 vec![UpgradeFromEntry {
14897 from: "0.0.1".into(),
14898 instructions: vec![UpgradeInstruction::Restart],
14899 }],
14900 vec![
14901 UpgradeFromEntry {
14902 from: "0.0.1".into(),
14903 instructions: vec![
14904 UpgradeInstruction::LoadModule {
14905 module: "demo".into(),
14906 },
14907 UpgradeInstruction::SoftPurge {
14908 module: "demo".into(),
14909 },
14910 ],
14911 },
14912 UpgradeFromEntry {
14913 from: "0.0.2".into(),
14914 instructions: vec![
14915 UpgradeInstruction::StateChange {
14916 script: "servicos/upgrade.lisp".into(),
14917 },
14918 UpgradeInstruction::Purge {
14919 module: "demo".into(),
14920 },
14921 UpgradeInstruction::Restart,
14922 ],
14923 },
14924 ],
14925 vec![
14926 UpgradeFromEntry {
14927 from: "0.1.0".into(),
14928 instructions: vec![UpgradeInstruction::Restart],
14929 },
14930 UpgradeFromEntry {
14931 from: "0.1.0".into(),
14932 instructions: vec![UpgradeInstruction::Restart],
14933 },
14934 ],
14935 ];
14936 for upgrade_from in fixtures {
14937 let c = caixa_with_upgrade_from(upgrade_from.clone());
14938 assert_eq!(
14939 c.upgrade_from(),
14940 upgrade_from.as_slice(),
14941 "Caixa::upgrade_from must return :upgrade-from \
14942 verbatim (got {:?}, expected {upgrade_from:?})",
14943 c.upgrade_from(),
14944 );
14945 assert_eq!(
14946 c.upgrade_from(),
14947 c.upgrade_from.as_slice(),
14948 "Caixa::upgrade_from must element-equal the raw \
14949 `self.upgrade_from.as_slice()` field access across \
14950 every value in the Vec<UpgradeFromEntry> accept-set",
14951 );
14952 assert_eq!(
14953 c.upgrade_from().is_empty(),
14954 c.upgrade_from.is_empty(),
14955 "Caixa::upgrade_from().is_empty() must byte-equal \
14956 self.upgrade_from.is_empty() — a presence-bit drift \
14957 would silently split the paired \
14958 Caixa::declared_servico_slots M2 declared-slot \
14959 enumerator's presence probe from the peer \
14960 crate::render::servico_m2_overlay M2 overlay \
14961 emitter's presence gate",
14962 );
14963 }
14964 }
14965
14966 #[test]
14967 fn declared_servico_slots_upgrade_from_arm_routes_through_accessor() {
14968 // Composition pin: [`Caixa::declared_servico_slots`]'s
14969 // `:upgrade-from` presence-probe arm must key off
14970 // [`Caixa::upgrade_from`], not the raw
14971 // `self.upgrade_from.is_empty()` field-probe. Structurally: a
14972 // `Caixa { upgrade_from: vec![UpgradeFromEntry { from: "0.0.1",
14973 // instructions: vec![Restart] }], .. }` must push
14974 // `M2_AUTHOR_KEY_UPGRADE_FROM` onto the declared-slot list
14975 // (the presence bit is non-empty, so the M2 kind-coherence
14976 // gate must surface the slot as "declared"), and a `Caixa {
14977 // upgrade_from: vec![], .. }` must NOT push the label (the
14978 // "author omitted the slot entirely" arm — the empty-slice
14979 // partition the serde-default folds onto). The pair jointly
14980 // pins the accessor + declared-slot enumerator composition:
14981 // any future silent detour that had the accessor collapse
14982 // `[Restart]` to `[]` (a `.filter(|e| !e.instructions.
14983 // is_empty())` projection) would silently absorb the
14984 // "declared but degenerate" arm at the accessor boundary and
14985 // the [`crate::LayoutError::ServicoSlotsOnNonServico`] kind-
14986 // coherence gate would silently accept a struct-literal
14987 // `Caixa` carrying the drift.
14988 //
14989 // Peer of the sibling
14990 // `declared_servico_slots_limits_arm_routes_through_accessor`
14991 // (b2bd9d7) and
14992 // `declared_servico_slots_behavior_arm_routes_through_accessor`
14993 // (35d8b52) composition pins on the sibling `:limits` /
14994 // `:behavior` outer-`Option<&Composite>` arms — same "the
14995 // enumerator gate must route through the substrate-primitive
14996 // typed dispatch" discipline extended onto the third M2
14997 // Servico-runtime slot axis, closing the enumerator's routing
14998 // invariant on every M2 arm.
14999 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
15000 let c = caixa_with_upgrade_from(vec![UpgradeFromEntry {
15001 from: "0.0.1".into(),
15002 instructions: vec![UpgradeInstruction::Restart],
15003 }]);
15004 let slots = c.declared_servico_slots();
15005 assert!(
15006 slots.contains(&crate::render::M2_AUTHOR_KEY_UPGRADE_FROM),
15007 "declared_servico_slots must push \
15008 M2_AUTHOR_KEY_UPGRADE_FROM when `:upgrade-from` is \
15009 non-empty — the accessor and the enumerator gate must \
15010 route through the same substrate-primitive typed \
15011 dispatch on the outer :upgrade-from presence bit (got \
15012 slots={slots:?})",
15013 );
15014 let c = caixa_with_upgrade_from(vec![]);
15015 let slots = c.declared_servico_slots();
15016 assert!(
15017 !slots.contains(&crate::render::M2_AUTHOR_KEY_UPGRADE_FROM),
15018 "declared_servico_slots must NOT push \
15019 M2_AUTHOR_KEY_UPGRADE_FROM when `:upgrade-from` is \
15020 empty — the author-omitted arm must route through the \
15021 accessor's empty-slice return unchanged (got \
15022 slots={slots:?})",
15023 );
15024 }
15025
15026 #[test]
15027 fn servico_m2_overlay_upgrade_from_arm_routes_through_accessor() {
15028 // Composition pin: [`crate::render::servico_m2_overlay`]'s
15029 // per-`:upgrade-from` M2 overlay emit arm must key off
15030 // [`Caixa::upgrade_from`], not the raw
15031 // `!caixa.upgrade_from.is_empty()` presence gate + the
15032 // `serde_yaml::to_value(&caixa.upgrade_from)` projection.
15033 // Structurally: a `Caixa { upgrade_from: vec![UpgradeFromEntry
15034 // { from: "0.0.1", instructions: vec![Restart] }], .. }` must
15035 // surface the `M2_KEY_UPGRADE_FROM` key with a per-entry
15036 // sequence in the overlay (the emitter fans onto the serde
15037 // slice-serialization), and a `Caixa { upgrade_from: vec![],
15038 // .. }` must omit the key entirely (the empty-slice
15039 // partition — the `!.is_empty()` outer gate elides the key
15040 // when the author omitted the slot). The pair jointly pins
15041 // the accessor + M2 overlay emitter composition: any future
15042 // silent detour that had the accessor return a fresh-cloned
15043 // `Vec<UpgradeFromEntry>` copy would silently break the
15044 // reference-identity pin the peer per-entry
15045 // `serde_yaml::to_value(caixa.upgrade_from())` projection
15046 // reads from — the projection would clone once per accessor
15047 // call instead of borrowing the storage buffer verbatim.
15048 //
15049 // Peer of the sibling
15050 // `servico_m2_overlay_limits_arm_routes_through_accessor`
15051 // (b2bd9d7) and
15052 // `servico_m2_overlay_behavior_arm_routes_through_accessor`
15053 // (35d8b52) composition pins on the sibling `:limits` /
15054 // `:behavior` outer-`Option<&Composite>` arms — same "the
15055 // M2 overlay emitter must route through the substrate-
15056 // primitive typed dispatch" discipline extended onto the
15057 // third M2 Servico-runtime slot axis, closing the overlay
15058 // emitter's routing invariant on every M2 arm.
15059 use crate::render::{M2_KEY_UPGRADE_FROM, servico_m2_overlay};
15060 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
15061 let c = caixa_with_upgrade_from(vec![UpgradeFromEntry {
15062 from: "0.0.1".into(),
15063 instructions: vec![UpgradeInstruction::Restart],
15064 }]);
15065 let overlay = servico_m2_overlay(&c).unwrap();
15066 assert!(
15067 overlay.contains_key(M2_KEY_UPGRADE_FROM),
15068 "servico_m2_overlay must surface M2_KEY_UPGRADE_FROM when \
15069 `:upgrade-from` is non-empty — the accessor and the M2 \
15070 overlay emitter must route through the same substrate- \
15071 primitive typed dispatch on the outer :upgrade-from \
15072 slice (got overlay={overlay:?})",
15073 );
15074 let c = caixa_with_upgrade_from(vec![]);
15075 let overlay = servico_m2_overlay(&c).unwrap();
15076 assert!(
15077 !overlay.contains_key(M2_KEY_UPGRADE_FROM),
15078 "servico_m2_overlay must omit M2_KEY_UPGRADE_FROM when \
15079 `:upgrade-from` is empty — the empty-slice partition \
15080 must route through the accessor's empty-slice return \
15081 unchanged (got overlay={overlay:?})",
15082 );
15083 }
15084
15085 #[test]
15086 fn upgrade_from_projects_slice_by_borrow() {
15087 // The by-borrow pin: [`Caixa::upgrade_from`] returns
15088 // `&[UpgradeFromEntry]` by borrow — the returned slice
15089 // borrows the underlying `Vec<UpgradeFromEntry>` storage of
15090 // the `:upgrade-from` slot and the accessor must not clone
15091 // the backing `Vec` on every call. Peer of the sibling
15092 // outer top-level [`Caixa`] `&[T]`-return by-borrow pins
15093 // (`autores_projects_slice_by_borrow` b5d813f,
15094 // `etiquetas_projects_slice_by_borrow` 78c7d3c,
15095 // `bibliotecas_projects_slice_by_borrow` 8a36c23,
15096 // `exe_projects_slice_by_borrow` 65d9527,
15097 // `servicos_projects_slice_by_borrow` 611f78b,
15098 // `deps_projects_slice_by_borrow` ad34b4e,
15099 // `deps_dev_projects_slice_by_borrow` f7fd81e) on the
15100 // sibling outer top-level [`Caixa`] scalar-element `&[T]`
15101 // axes — extended here to the first outer-`Caixa`
15102 // composite-element `&[Composite]` axis: the accessor's
15103 // returned slice must borrow from `&self` (the returned
15104 // reference's lifetime is tied to `&self`), and calling the
15105 // accessor twice on the same [`Caixa`] must yield slices
15106 // that are pointer-equal (the underlying byte-buffer is the
15107 // storage `Vec`'s allocation, not a fresh copy) as well as
15108 // value-equal (idempotent, no side effects on `&self`).
15109 //
15110 // Pins against a future silent detour that returned an owned
15111 // `Vec<UpgradeFromEntry>` (which would type-check but
15112 // silently clone on every call), a `&Vec<UpgradeFromEntry>`
15113 // return (which would leak the backing `Vec`'s
15114 // grow/push/reserve surface no downstream consumer reaches
15115 // for), or a one-arm-only accessor that returned a
15116 // saturating value on some sentinel input.
15117 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
15118 for upgrade_from in [
15119 vec![],
15120 vec![UpgradeFromEntry {
15121 from: "0.0.1".into(),
15122 instructions: vec![UpgradeInstruction::Restart],
15123 }],
15124 vec![
15125 UpgradeFromEntry {
15126 from: "0.0.1".into(),
15127 instructions: vec![UpgradeInstruction::Restart],
15128 },
15129 UpgradeFromEntry {
15130 from: "0.0.2".into(),
15131 instructions: vec![UpgradeInstruction::SoftPurge {
15132 module: "demo".into(),
15133 }],
15134 },
15135 ],
15136 ] {
15137 let c = caixa_with_upgrade_from(upgrade_from.clone());
15138 let first = c.upgrade_from();
15139 let second = c.upgrade_from();
15140 assert_eq!(
15141 first, second,
15142 "Caixa::upgrade_from must be idempotent — two \
15143 successive calls on the same &self must return the \
15144 same &[UpgradeFromEntry]",
15145 );
15146 assert_eq!(
15147 first.as_ptr(),
15148 second.as_ptr(),
15149 "Caixa::upgrade_from must borrow the underlying \
15150 Vec<UpgradeFromEntry> storage — two successive calls \
15151 must return slices with the same backing pointer (a \
15152 fresh Vec<UpgradeFromEntry> clone would change the \
15153 pointer on every call)",
15154 );
15155 assert_eq!(
15156 first,
15157 upgrade_from.as_slice(),
15158 "Caixa::upgrade_from must return :upgrade-from \
15159 verbatim by borrow — got {first:?}, expected \
15160 {upgrade_from:?}",
15161 );
15162 }
15163 }
15164
15165 // ── Caixa::children — outer top-level &[ChildSpec] composite-slice accessor ──
15166
15167 fn caixa_with_children(children: Vec<crate::supervisor::ChildSpec>) -> Caixa {
15168 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
15169 c.children = children;
15170 c
15171 }
15172
15173 #[test]
15174 fn children_returns_children_slice_verbatim_across_permutations() {
15175 // The canonical per-`Caixa` `:children` M2 supervisor-tree-slot
15176 // outer-composite `&[ChildSpec]`-return slice-shape pin:
15177 // [`Caixa::children`] must return the `:children` typed
15178 // `Vec<ChildSpec>` verbatim as a `&[ChildSpec]` slice-view over
15179 // the same backing buffer the raw `self.children.as_slice()`
15180 // field access borrows from, element-equal across every
15181 // representative fixture in the accept-set — `[]` (the "no
15182 // static children declared" arm every non-`Supervisor`-kind
15183 // `defcaixa` carries by `#[serde(default)]` and every
15184 // `SimpleOneForOne` supervisor carries by cross-slot refusal),
15185 // a canonical single-child `Permanent` fixture (the shape
15186 // most `OneForOne` supervisors carry — a single long-running
15187 // worker child), a canonical multi-child list carrying every
15188 // typed restart-policy variant (`Permanent` / `Transient` /
15189 // `Temporary`), and a past-the-guard sentinel — a duplicate
15190 // `:caixa` `[("w", ...), ("w", ...)]` entry pair
15191 // ([`crate::SupervisorSpec::validate`] rejects through
15192 // `DuplicateChildNome { nome: "w" }` but the accessor must
15193 // ship the raw slot verbatim so struct-literal fixtures
15194 // continue to expose the duplicate at the accessor boundary).
15195 //
15196 // Pins against a future silent detour that returned an owned
15197 // `Vec<ChildSpec>` (which would type-check but silently clone
15198 // on every accessor call, breaking the zero-cost projection
15199 // every peer sibling slice accessor carries), a `[dup, dup] →
15200 // [dup]` dedup collapse (which would silently absorb the
15201 // `DuplicateChildNome` refusal case at the accessor boundary
15202 // and the [`crate::StandardLayout::verify`] cross-child gate
15203 // would silently accept a struct-literal `Caixa` carrying the
15204 // drift), a reference to an operator-resolved overlay (the
15205 // future per-cluster `:children-overrides` slot — its
15206 // resolution must land at exactly this accessor body, not
15207 // silently divert the raw slot away from a second consumer),
15208 // or an axis-shuffled projection (a future detour that
15209 // reordered children through the accessor would silently
15210 // split the paired [`crate::StandardLayout::verify`] per-
15211 // supervisor gate's traversal input from the peer
15212 // [`Self::supervisor_view`] fold-in path's clone-order input,
15213 // since the OTP `RestForOne` restart strategy dispatches on
15214 // declared child order and axis reordering would silently
15215 // split the operator's per-cluster restart-fan-out order
15216 // from the caixa.lisp source-order).
15217 //
15218 // Second outer top-level [`Caixa`] `&[Composite]`-return slice
15219 // accessor pin on the substrate primitive for M2 / M3 typed-
15220 // slot vec-carry axes — folds on the outer-`Caixa`
15221 // `&[Composite]` composite-slice sub-family the sibling
15222 // `upgrade_from_returns_upgrade_from_slice_verbatim_across_permutations`
15223 // (2a1f907) pin opened, peer at the outer altitude of the
15224 // closed inner-`SupervisorSpec` `SupervisorSpec::children`
15225 // (bc92bce) accessor on the same OTP-supervisor static-child-
15226 // list axis.
15227 use crate::supervisor::{ChildSpec, RestartPolicy};
15228 let fixtures: Vec<Vec<ChildSpec>> = vec![
15229 vec![],
15230 vec![ChildSpec {
15231 caixa: "worker".into(),
15232 versao: "^0.1".into(),
15233 restart: RestartPolicy::Permanent,
15234 }],
15235 vec![
15236 ChildSpec {
15237 caixa: "worker-a".into(),
15238 versao: "^0.1".into(),
15239 restart: RestartPolicy::Permanent,
15240 },
15241 ChildSpec {
15242 caixa: "worker-b".into(),
15243 versao: "^0.1".into(),
15244 restart: RestartPolicy::Transient,
15245 },
15246 ChildSpec {
15247 caixa: "worker-c".into(),
15248 versao: "^0.1".into(),
15249 restart: RestartPolicy::Temporary,
15250 },
15251 ],
15252 vec![
15253 ChildSpec {
15254 caixa: "w".into(),
15255 versao: "^0.1".into(),
15256 restart: RestartPolicy::Permanent,
15257 },
15258 ChildSpec {
15259 caixa: "w".into(),
15260 versao: "^0.1".into(),
15261 restart: RestartPolicy::Permanent,
15262 },
15263 ],
15264 ];
15265 for children in fixtures {
15266 let c = caixa_with_children(children.clone());
15267 assert_eq!(
15268 c.children(),
15269 children.as_slice(),
15270 "Caixa::children must return :children verbatim \
15271 (got {:?}, expected {children:?})",
15272 c.children(),
15273 );
15274 assert_eq!(
15275 c.children(),
15276 c.children.as_slice(),
15277 "Caixa::children must element-equal the raw \
15278 `self.children.as_slice()` field access across \
15279 every value in the Vec<ChildSpec> accept-set",
15280 );
15281 assert_eq!(
15282 c.children().is_empty(),
15283 c.children.is_empty(),
15284 "Caixa::children().is_empty() must byte-equal \
15285 self.children.is_empty() — a presence-bit drift \
15286 would silently split the paired \
15287 Caixa::declared_supervisor_slots supervisor-tree \
15288 declared-slot enumerator's presence probe from the \
15289 peer Caixa::supervisor_view typed-view composer's \
15290 fold-in path",
15291 );
15292 }
15293 }
15294
15295 #[test]
15296 fn declared_supervisor_slots_children_arm_routes_through_accessor() {
15297 // Composition pin: [`Caixa::declared_supervisor_slots`]'s
15298 // `:children` presence-probe arm must key off
15299 // [`Caixa::children`], not the raw
15300 // `!self.children.is_empty()` field-probe. Structurally: a
15301 // `Caixa { children: vec![ChildSpec { caixa: "w", versao:
15302 // "^0.1", restart: Permanent }], .. }` must push
15303 // `SUPERVISOR_AUTHOR_KEY_CHILDREN` onto the declared-slot list
15304 // (the presence bit is non-empty, so the supervisor-tree
15305 // kind-coherence gate must surface the slot as "declared"),
15306 // and a `Caixa { children: vec![], .. }` must NOT push the
15307 // label (the "author omitted the slot entirely" arm — the
15308 // empty-slice partition the serde-default folds onto). The
15309 // pair jointly pins the accessor + declared-slot enumerator
15310 // composition: any future silent detour that had the accessor
15311 // collapse `[Permanent]` to `[]` (a `.filter(|c| c.nome() !=
15312 // "__reserved__")` projection) would silently absorb the
15313 // "declared but degenerate" arm at the accessor boundary and
15314 // the [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
15315 // kind-coherence gate would silently accept a struct-literal
15316 // `Caixa` carrying the drift.
15317 //
15318 // Peer of the sibling
15319 // `declared_servico_slots_upgrade_from_arm_routes_through_accessor`
15320 // (2a1f907) on the M2 `:upgrade-from` composite-slice arm —
15321 // same "the enumerator gate must route through the substrate-
15322 // primitive typed dispatch" discipline extended onto the
15323 // supervisor-tree `:children` composite-slice arm.
15324 use crate::supervisor::{ChildSpec, RestartPolicy};
15325 let c = caixa_with_children(vec![ChildSpec {
15326 caixa: "w".into(),
15327 versao: "^0.1".into(),
15328 restart: RestartPolicy::Permanent,
15329 }]);
15330 let slots = c.declared_supervisor_slots();
15331 assert!(
15332 slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN),
15333 "declared_supervisor_slots must push \
15334 SUPERVISOR_AUTHOR_KEY_CHILDREN when `:children` is \
15335 non-empty — the accessor and the enumerator gate must \
15336 route through the same substrate-primitive typed \
15337 dispatch on the outer :children presence bit (got \
15338 slots={slots:?})",
15339 );
15340 let c = caixa_with_children(vec![]);
15341 let slots = c.declared_supervisor_slots();
15342 assert!(
15343 !slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN),
15344 "declared_supervisor_slots must NOT push \
15345 SUPERVISOR_AUTHOR_KEY_CHILDREN when `:children` is \
15346 empty — the author-omitted arm must route through the \
15347 accessor's empty-slice return unchanged (got \
15348 slots={slots:?})",
15349 );
15350 }
15351
15352 #[test]
15353 fn supervisor_view_children_arm_routes_through_accessor() {
15354 // Composition pin: [`Caixa::supervisor_view`]'s per-`:children`
15355 // fold-in arm must key off [`Caixa::children`], not the raw
15356 // `self.children.clone()` field-clone. Structurally: a `Caixa {
15357 // kind: Supervisor, estrategia: Some(OneForOne), children:
15358 // vec![ChildSpec { caixa: "w", .. }], .. }` must fold the
15359 // per-child list through the accessor into the typed
15360 // [`SupervisorSpec`] view's `children` field verbatim — every
15361 // entry the accessor surfaces must land in the view's
15362 // `children` slot in the same order. The pair jointly pins the
15363 // accessor + view-composer composition: any future silent
15364 // detour that had the accessor return a fresh-cloned
15365 // `Vec<ChildSpec>` copy would silently break the reference-
15366 // identity pin the peer `supervisor_view` fold-in path reads
15367 // from — the fold would clone once more per accessor call
15368 // instead of borrowing the storage buffer verbatim once.
15369 //
15370 // Peer of the sibling
15371 // `supervisor_view_kind_gate_routes_through_accessor` (35d8b52-
15372 // family) composition pin on the peer kind-gate arm — same
15373 // "the view composer must route through the substrate-
15374 // primitive typed dispatch" discipline extended onto the
15375 // per-`:children` fold-in arm, closing the supervisor-view
15376 // composer's routing invariant on the composite-slice input.
15377 use crate::supervisor::{ChildSpec, RestartPolicy, RestartStrategy};
15378 let mut c = caixa_with_children(vec![
15379 ChildSpec {
15380 caixa: "worker-a".into(),
15381 versao: "^0.1".into(),
15382 restart: RestartPolicy::Permanent,
15383 },
15384 ChildSpec {
15385 caixa: "worker-b".into(),
15386 versao: "^0.1".into(),
15387 restart: RestartPolicy::Transient,
15388 },
15389 ]);
15390 c.kind = crate::CaixaKind::Supervisor;
15391 c.estrategia = Some(RestartStrategy::OneForOne);
15392 let view = c
15393 .supervisor_view()
15394 .expect("Supervisor kind must produce a supervisor_view");
15395 assert_eq!(
15396 view.children(),
15397 c.children(),
15398 "supervisor_view must fold Caixa::children verbatim into \
15399 SupervisorSpec::children — the accessor and the view \
15400 composer must route through the same substrate-primitive \
15401 typed dispatch on the outer :children slice (got view \
15402 children={:?}, expected {:?})",
15403 view.children(),
15404 c.children(),
15405 );
15406 }
15407
15408 #[test]
15409 fn children_projects_slice_by_borrow() {
15410 // The by-borrow pin: [`Caixa::children`] returns
15411 // `&[ChildSpec]` by borrow — the returned slice borrows the
15412 // underlying `Vec<ChildSpec>` storage of the `:children` slot
15413 // and the accessor must not clone the backing `Vec` on every
15414 // call. Peer of the sibling outer top-level [`Caixa`]
15415 // `&[T]`-return by-borrow pins (`autores_projects_slice_by_borrow`
15416 // b5d813f, `etiquetas_projects_slice_by_borrow` 78c7d3c,
15417 // `bibliotecas_projects_slice_by_borrow` 8a36c23,
15418 // `exe_projects_slice_by_borrow` 65d9527,
15419 // `servicos_projects_slice_by_borrow` 611f78b,
15420 // `deps_projects_slice_by_borrow` ad34b4e,
15421 // `deps_dev_projects_slice_by_borrow` f7fd81e,
15422 // `upgrade_from_projects_slice_by_borrow` 2a1f907) on the
15423 // sibling outer top-level [`Caixa`] scalar-element and
15424 // composite-element `&[T]` axes — folds on the outer-`Caixa`
15425 // composite-element `&[Composite]` axis: the accessor's
15426 // returned slice must borrow from `&self` (the returned
15427 // reference's lifetime is tied to `&self`), and calling the
15428 // accessor twice on the same [`Caixa`] must yield slices
15429 // that are pointer-equal (the underlying byte-buffer is the
15430 // storage `Vec`'s allocation, not a fresh copy) as well as
15431 // value-equal (idempotent, no side effects on `&self`).
15432 //
15433 // Pins against a future silent detour that returned an owned
15434 // `Vec<ChildSpec>` (which would type-check but silently clone
15435 // on every call), a `&Vec<ChildSpec>` return (which would leak
15436 // the backing `Vec`'s grow/push/reserve surface no downstream
15437 // consumer reaches for), or a one-arm-only accessor that
15438 // returned a saturating value on some sentinel input.
15439 use crate::supervisor::{ChildSpec, RestartPolicy};
15440 for children in [
15441 vec![],
15442 vec![ChildSpec {
15443 caixa: "w".into(),
15444 versao: "^0.1".into(),
15445 restart: RestartPolicy::Permanent,
15446 }],
15447 vec![
15448 ChildSpec {
15449 caixa: "worker-a".into(),
15450 versao: "^0.1".into(),
15451 restart: RestartPolicy::Permanent,
15452 },
15453 ChildSpec {
15454 caixa: "worker-b".into(),
15455 versao: "^0.1".into(),
15456 restart: RestartPolicy::Transient,
15457 },
15458 ],
15459 ] {
15460 let c = caixa_with_children(children.clone());
15461 let first = c.children();
15462 let second = c.children();
15463 assert_eq!(
15464 first, second,
15465 "Caixa::children must be idempotent — two successive \
15466 calls on the same &self must return the same \
15467 &[ChildSpec]",
15468 );
15469 assert_eq!(
15470 first.as_ptr(),
15471 second.as_ptr(),
15472 "Caixa::children must borrow the underlying \
15473 Vec<ChildSpec> storage — two successive calls must \
15474 return slices with the same backing pointer (a fresh \
15475 Vec<ChildSpec> clone would change the pointer on \
15476 every call)",
15477 );
15478 assert_eq!(
15479 first,
15480 children.as_slice(),
15481 "Caixa::children must return :children verbatim by \
15482 borrow — got {first:?}, expected {children:?}",
15483 );
15484 }
15485 }
15486
15487 // ── Caixa::membros — outer top-level &[Membro] composite-slice accessor ──
15488
15489 fn caixa_aplicacao_with_membros(membros: Vec<crate::aplicacao::Membro>) -> Caixa {
15490 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
15491 c.kind = CaixaKind::Aplicacao;
15492 c.membros = membros;
15493 c
15494 }
15495
15496 #[test]
15497 fn membros_returns_membros_slice_verbatim_across_permutations() {
15498 // The canonical per-`Caixa` `:membros` M3 mesh-slot outer-
15499 // composite `&[Membro]`-return slice-shape pin:
15500 // [`Caixa::membros`] must return the `:membros` typed
15501 // `Vec<Membro>` verbatim as a `&[Membro]` slice-view over the
15502 // same backing buffer the raw `self.membros.as_slice()` field
15503 // access borrows from, element-equal across every
15504 // representative fixture in the accept-set — `[]` (the "no
15505 // members declared" arm every non-`Aplicacao`-kind `defcaixa`
15506 // carries by `#[serde(default)]` and every partially-authored
15507 // Aplicacao carries before the
15508 // [`crate::AplicacaoError::MembrosEmpty`] gate fires), a
15509 // canonical single-member fixture (the shape a minimal
15510 // Aplicacao carries — one Servico wrapping one contained
15511 // computation), a canonical multi-member list carrying three
15512 // distinct entries (the canonical checkout-shape Aplicacao —
15513 // cart / pricing / auth — every canonical example carries), and
15514 // a past-the-guard sentinel — a duplicate `:caixa`
15515 // `[("cart", ...), ("cart", ...)]` entry pair
15516 // ([`crate::AplicacaoSpec::validate`] rejects through
15517 // `DuplicateMembro { nome: "cart" }` but the accessor must ship
15518 // the raw slot verbatim so struct-literal fixtures continue to
15519 // expose the duplicate at the accessor boundary).
15520 //
15521 // Pins against a future silent detour that returned an owned
15522 // `Vec<Membro>` (which would type-check but silently clone on
15523 // every accessor call, breaking the zero-cost projection every
15524 // peer sibling slice accessor carries), a `[dup, dup] → [dup]`
15525 // dedup collapse (which would silently absorb the
15526 // `DuplicateMembro` refusal case at the accessor boundary and
15527 // the [`crate::StandardLayout::verify`] cross-member gate would
15528 // silently accept a struct-literal `Caixa` carrying the drift),
15529 // a reference to an operator-resolved overlay (the future per-
15530 // cluster `:membros-overrides` slot — its resolution must land
15531 // at exactly this accessor body, not silently divert the raw
15532 // slot away from a second consumer), or an axis-shuffled
15533 // projection (a future detour that reordered members through
15534 // the accessor would silently split the paired
15535 // [`crate::StandardLayout::verify`] per-Aplicacao gate's
15536 // traversal input from the peer [`Self::aplicacao_view`] fold-
15537 // in path's clone-order input, since the canonical `:contratos`
15538 // `:de`/`:para` and `:entrada :para` cross-slot refusal probes
15539 // read the member set through the same slice).
15540 //
15541 // Third outer top-level [`Caixa`] `&[Composite]`-return slice
15542 // accessor pin on the substrate primitive for M2 / M3 typed-
15543 // slot vec-carry axes — opens the outer-`Caixa` M3 mesh-slot
15544 // arm of the `&[Composite]` composite-slice sub-family the
15545 // sibling M2 `upgrade_from_returns_upgrade_from_slice_verbatim_across_permutations`
15546 // (2a1f907) and
15547 // `children_returns_children_slice_verbatim_across_permutations`
15548 // (c17b51e) pins opened, peer at the outer altitude of the
15549 // closed inner-[`crate::AplicacaoSpec::membros`] (6c77e36)
15550 // accessor on the same MESH-COMPOSITION per-Aplicacao member-
15551 // list axis.
15552 use crate::aplicacao::Membro;
15553 let fixtures: Vec<Vec<Membro>> = vec![
15554 vec![],
15555 vec![Membro {
15556 caixa: "cart".into(),
15557 versao: "^0.1".into(),
15558 }],
15559 vec![
15560 Membro {
15561 caixa: "cart".into(),
15562 versao: "^0.1".into(),
15563 },
15564 Membro {
15565 caixa: "pricing".into(),
15566 versao: "^0.2".into(),
15567 },
15568 Membro {
15569 caixa: "auth".into(),
15570 versao: "^1.0".into(),
15571 },
15572 ],
15573 vec![
15574 Membro {
15575 caixa: "cart".into(),
15576 versao: "^0.1".into(),
15577 },
15578 Membro {
15579 caixa: "cart".into(),
15580 versao: "^0.1".into(),
15581 },
15582 ],
15583 ];
15584 for membros in fixtures {
15585 let c = caixa_aplicacao_with_membros(membros.clone());
15586 assert_eq!(
15587 c.membros(),
15588 membros.as_slice(),
15589 "Caixa::membros must return :membros verbatim \
15590 (got {:?}, expected {membros:?})",
15591 c.membros(),
15592 );
15593 assert_eq!(
15594 c.membros(),
15595 c.membros.as_slice(),
15596 "Caixa::membros must element-equal the raw \
15597 `self.membros.as_slice()` field access across every \
15598 value in the Vec<Membro> accept-set",
15599 );
15600 assert_eq!(
15601 c.membros().is_empty(),
15602 c.membros.is_empty(),
15603 "Caixa::membros().is_empty() must byte-equal \
15604 self.membros.is_empty() — a presence-bit drift would \
15605 silently split the paired Caixa::declared_mesh_slots \
15606 mesh declared-slot enumerator's presence probe from \
15607 the peer Caixa::aplicacao_view typed-view composer's \
15608 fold-in path",
15609 );
15610 }
15611 }
15612
15613 #[test]
15614 fn declared_mesh_slots_membros_arm_routes_through_accessor() {
15615 // Composition pin: [`Caixa::declared_mesh_slots`]'s `:membros`
15616 // presence-probe arm must key off [`Caixa::membros`], not the
15617 // raw `!self.membros.is_empty()` field-probe. Structurally: a
15618 // `Caixa { membros: vec![Membro { caixa: "cart", versao:
15619 // "^0.1" }], .. }` must push `M3_AUTHOR_KEY_MEMBROS` onto the
15620 // declared-slot list (the presence bit is non-empty, so the
15621 // mesh kind-coherence gate must surface the slot as
15622 // "declared"), and a `Caixa { membros: vec![], .. }` must NOT
15623 // push the label (the "author omitted the slot entirely" arm
15624 // — the empty-slice partition the serde-default folds onto).
15625 // The pair jointly pins the accessor + declared-slot
15626 // enumerator composition: any future silent detour that had
15627 // the accessor collapse `[Membro { .. }]` to `[]` (a
15628 // `.filter(|m| m.nome() != "__reserved__")` projection) would
15629 // silently absorb the "declared but degenerate" arm at the
15630 // accessor boundary and the
15631 // [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
15632 // coherence gate would silently accept a struct-literal
15633 // `Caixa` carrying the drift.
15634 //
15635 // Peer of the sibling
15636 // `declared_servico_slots_upgrade_from_arm_routes_through_accessor`
15637 // (2a1f907) and
15638 // `declared_supervisor_slots_children_arm_routes_through_accessor`
15639 // (c17b51e) composition pins on the M2 `:upgrade-from` /
15640 // `:children` composite-slice arms — same "the enumerator gate
15641 // must route through the substrate-primitive typed dispatch"
15642 // discipline extended onto the M3 `:membros` composite-slice
15643 // arm, opening the M3 arm of the declared-slot enumerator's
15644 // routing invariant.
15645 use crate::aplicacao::Membro;
15646 let c = caixa_aplicacao_with_membros(vec![Membro {
15647 caixa: "cart".into(),
15648 versao: "^0.1".into(),
15649 }]);
15650 let slots = c.declared_mesh_slots();
15651 assert!(
15652 slots.contains(&crate::render::M3_AUTHOR_KEY_MEMBROS),
15653 "declared_mesh_slots must push M3_AUTHOR_KEY_MEMBROS when \
15654 `:membros` is non-empty — the accessor and the enumerator \
15655 gate must route through the same substrate-primitive \
15656 typed dispatch on the outer :membros presence bit (got \
15657 slots={slots:?})",
15658 );
15659 let c = caixa_aplicacao_with_membros(vec![]);
15660 let slots = c.declared_mesh_slots();
15661 assert!(
15662 !slots.contains(&crate::render::M3_AUTHOR_KEY_MEMBROS),
15663 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_MEMBROS \
15664 when `:membros` is empty — the author-omitted arm must \
15665 route through the accessor's empty-slice return unchanged \
15666 (got slots={slots:?})",
15667 );
15668 }
15669
15670 #[test]
15671 fn aplicacao_view_membros_arm_routes_through_accessor() {
15672 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:membros`
15673 // fold-in arm must key off [`Caixa::membros`], not the raw
15674 // `self.membros.clone()` field-clone. Structurally: a `Caixa {
15675 // kind: Aplicacao, membros: vec![Membro { caixa: "cart", .. },
15676 // Membro { caixa: "pricing", .. }], .. }` must fold the per-
15677 // member list through the accessor into the typed
15678 // [`crate::AplicacaoSpec`] view's `membros` slot verbatim —
15679 // every entry the accessor surfaces must land in the view's
15680 // `membros` slot in the same order. The pair jointly pins the
15681 // accessor + view-composer composition: any future silent
15682 // detour that had the accessor return a fresh-cloned
15683 // `Vec<Membro>` copy would silently break the reference-
15684 // identity pin the peer `aplicacao_view` fold-in path reads
15685 // from — the fold would clone once more per accessor call
15686 // instead of borrowing the storage buffer verbatim once.
15687 //
15688 // Peer of the sibling
15689 // `aplicacao_view_politicas_arm_folds_through_accessor`
15690 // (5d23d29) /
15691 // `aplicacao_view_placement_arm_folds_through_accessor`
15692 // (4fb8074) /
15693 // `aplicacao_view_entrada_arm_folds_through_accessor` (e4128e4)
15694 // composition pins on the M3 `:politicas` / `:placement` /
15695 // `:entrada` outer-`Option<&Composite>` arms — extended here to
15696 // the M3 `:membros` outer-`&[Composite]` composite-slice arm,
15697 // closing the aplicacao-view composer's routing invariant on
15698 // the composite-slice input.
15699 use crate::aplicacao::Membro;
15700 let c = caixa_aplicacao_with_membros(vec![
15701 Membro {
15702 caixa: "cart".into(),
15703 versao: "^0.1".into(),
15704 },
15705 Membro {
15706 caixa: "pricing".into(),
15707 versao: "^0.2".into(),
15708 },
15709 ]);
15710 let view = c
15711 .aplicacao_view()
15712 .expect("Aplicacao kind must produce an aplicacao_view");
15713 assert_eq!(
15714 view.membros(),
15715 c.membros(),
15716 "aplicacao_view must fold Caixa::membros verbatim into \
15717 AplicacaoSpec::membros — the accessor and the view \
15718 composer must route through the same substrate-primitive \
15719 typed dispatch on the outer :membros slice (got view \
15720 membros={:?}, expected {:?})",
15721 view.membros(),
15722 c.membros(),
15723 );
15724 }
15725
15726 #[test]
15727 fn membros_projects_slice_by_borrow() {
15728 // The by-borrow pin: [`Caixa::membros`] returns `&[Membro]` by
15729 // borrow — the returned slice borrows the underlying
15730 // `Vec<Membro>` storage of the `:membros` slot and the
15731 // accessor must not clone the backing `Vec` on every call.
15732 // Peer of the sibling outer top-level [`Caixa`] `&[T]`-return
15733 // by-borrow pins (`autores_projects_slice_by_borrow` b5d813f,
15734 // `etiquetas_projects_slice_by_borrow` 78c7d3c,
15735 // `bibliotecas_projects_slice_by_borrow` 8a36c23,
15736 // `exe_projects_slice_by_borrow` 65d9527,
15737 // `servicos_projects_slice_by_borrow` 611f78b,
15738 // `deps_projects_slice_by_borrow` ad34b4e,
15739 // `deps_dev_projects_slice_by_borrow` f7fd81e,
15740 // `upgrade_from_projects_slice_by_borrow` 2a1f907,
15741 // `children_projects_slice_by_borrow` c17b51e) on the sibling
15742 // outer top-level [`Caixa`] scalar-element and composite-
15743 // element `&[T]` axes — folds on the outer-`Caixa` M3 mesh-
15744 // slot composite-element `&[Composite]` axis: the accessor's
15745 // returned slice must borrow from `&self` (the returned
15746 // reference's lifetime is tied to `&self`), and calling the
15747 // accessor twice on the same [`Caixa`] must yield slices that
15748 // are pointer-equal (the underlying byte-buffer is the storage
15749 // `Vec`'s allocation, not a fresh copy) as well as value-equal
15750 // (idempotent, no side effects on `&self`).
15751 //
15752 // Pins against a future silent detour that returned an owned
15753 // `Vec<Membro>` (which would type-check but silently clone on
15754 // every call), a `&Vec<Membro>` return (which would leak the
15755 // backing `Vec`'s grow/push/reserve surface no downstream
15756 // consumer reaches for), or a one-arm-only accessor that
15757 // returned a saturating value on some sentinel input.
15758 use crate::aplicacao::Membro;
15759 for membros in [
15760 vec![],
15761 vec![Membro {
15762 caixa: "cart".into(),
15763 versao: "^0.1".into(),
15764 }],
15765 vec![
15766 Membro {
15767 caixa: "cart".into(),
15768 versao: "^0.1".into(),
15769 },
15770 Membro {
15771 caixa: "pricing".into(),
15772 versao: "^0.2".into(),
15773 },
15774 ],
15775 ] {
15776 let c = caixa_aplicacao_with_membros(membros.clone());
15777 let first = c.membros();
15778 let second = c.membros();
15779 assert_eq!(
15780 first, second,
15781 "Caixa::membros must be idempotent — two successive \
15782 calls on the same &self must return the same &[Membro]",
15783 );
15784 assert_eq!(
15785 first.as_ptr(),
15786 second.as_ptr(),
15787 "Caixa::membros must borrow the underlying Vec<Membro> \
15788 storage — two successive calls must return slices with \
15789 the same backing pointer (a fresh Vec<Membro> clone \
15790 would change the pointer on every call)",
15791 );
15792 assert_eq!(
15793 first,
15794 membros.as_slice(),
15795 "Caixa::membros must return :membros verbatim by borrow \
15796 — got {first:?}, expected {membros:?}",
15797 );
15798 }
15799 }
15800
15801 // ── Caixa::contratos — outer top-level &[WitContract] composite-slice accessor ──
15802
15803 fn caixa_aplicacao_with_contratos(contratos: Vec<crate::aplicacao::WitContract>) -> Caixa {
15804 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
15805 c.kind = CaixaKind::Aplicacao;
15806 c.contratos = contratos;
15807 c
15808 }
15809
15810 fn contrato_http_for_test(
15811 de: &str,
15812 para: &str,
15813 endpoint: &str,
15814 ) -> crate::aplicacao::WitContract {
15815 crate::aplicacao::WitContract {
15816 de: de.into(),
15817 para: para.into(),
15818 wit: "wasi:http/proxy".into(),
15819 endpoint: Some(endpoint.into()),
15820 subject: None,
15821 slot: None,
15822 }
15823 }
15824
15825 #[test]
15826 fn contratos_returns_contratos_slice_verbatim_across_permutations() {
15827 // The canonical per-`Caixa` `:contratos` M3 mesh-slot outer-
15828 // composite `&[WitContract]`-return slice-shape pin:
15829 // [`Caixa::contratos`] must return the `:contratos` typed
15830 // `Vec<WitContract>` verbatim as a `&[WitContract]` slice-view
15831 // over the same backing buffer the raw
15832 // `self.contratos.as_slice()` field access borrows from,
15833 // element-equal across every representative fixture in the
15834 // accept-set — `[]` (the "no contracts declared" arm every
15835 // non-`Aplicacao`-kind `defcaixa` carries by
15836 // `#[serde(default)]` and every leaf-Aplicacao with a single
15837 // member carries), a canonical single-edge fixture (the
15838 // minimal directed-graph shape: one HTTP-shape `(cart → catalog)`
15839 // edge), and a canonical multi-edge fixture with three distinct
15840 // edges (the checkout-shape Aplicacao's HTTP-fan pattern:
15841 // `(cart → catalog)`, `(cart → pricing)`, `(cart → auth)`).
15842 //
15843 // Pins against a future silent detour that returned an owned
15844 // `Vec<WitContract>` (which would type-check but silently clone
15845 // on every accessor call, breaking the zero-cost projection
15846 // every peer sibling slice accessor carries), an axis-shuffled
15847 // projection (a future detour that reordered edges through the
15848 // accessor would silently split the paired
15849 // [`crate::StandardLayout::verify`] per-Aplicacao gate's
15850 // traversal input from the peer [`Self::aplicacao_view`] fold-
15851 // in path's clone-order input, since every canonical
15852 // `caixa-mesh` renderer's per-`(:de, :para)` adjacency-list
15853 // seed dispatch reads the edge set through the same slice),
15854 // or a reference to an operator-resolved overlay (the future
15855 // per-cluster `:contratos-overrides` slot — its resolution
15856 // must land at exactly this accessor body, not silently divert
15857 // the raw slot away from a second consumer).
15858 //
15859 // Fourth outer top-level [`Caixa`] `&[Composite]`-return slice
15860 // accessor pin on the substrate primitive for M2 / M3 typed-
15861 // slot vec-carry axes — closes the outer-`Caixa`
15862 // `&[Composite]` composite-slice sub-family the sibling M2
15863 // `upgrade_from_returns_upgrade_from_slice_verbatim_across_permutations`
15864 // (2a1f907) and
15865 // `children_returns_children_slice_verbatim_across_permutations`
15866 // (c17b51e) pins opened and the M3
15867 // `membros_returns_membros_slice_verbatim_across_permutations`
15868 // (0f26987) pin folded on, closing the outer-`Caixa` M3 mesh-
15869 // slot arm of the composite-slice sub-family. Peer at the outer
15870 // altitude of the closed inner-
15871 // [`crate::AplicacaoSpec::contratos`] (0dcc926) accessor on the
15872 // same MESH-COMPOSITION per-Aplicacao contract-list axis.
15873 let fixtures: Vec<Vec<crate::aplicacao::WitContract>> = vec![
15874 vec![],
15875 vec![contrato_http_for_test("cart", "catalog", "/items")],
15876 vec![
15877 contrato_http_for_test("cart", "catalog", "/items"),
15878 contrato_http_for_test("cart", "pricing", "/price"),
15879 contrato_http_for_test("cart", "auth", "/whoami"),
15880 ],
15881 ];
15882 for contratos in fixtures {
15883 let c = caixa_aplicacao_with_contratos(contratos.clone());
15884 assert_eq!(
15885 c.contratos(),
15886 contratos.as_slice(),
15887 "Caixa::contratos must return :contratos verbatim \
15888 (got {:?}, expected {contratos:?})",
15889 c.contratos(),
15890 );
15891 assert_eq!(
15892 c.contratos(),
15893 c.contratos.as_slice(),
15894 "Caixa::contratos must element-equal the raw \
15895 `self.contratos.as_slice()` field access across every \
15896 value in the Vec<WitContract> accept-set",
15897 );
15898 assert_eq!(
15899 c.contratos().is_empty(),
15900 c.contratos.is_empty(),
15901 "Caixa::contratos().is_empty() must byte-equal \
15902 self.contratos.is_empty() — a presence-bit drift would \
15903 silently split the paired Caixa::declared_mesh_slots \
15904 mesh declared-slot enumerator's presence probe from \
15905 the peer Caixa::aplicacao_view typed-view composer's \
15906 fold-in path",
15907 );
15908 }
15909 }
15910
15911 #[test]
15912 fn declared_mesh_slots_contratos_arm_routes_through_accessor() {
15913 // Composition pin: [`Caixa::declared_mesh_slots`]'s `:contratos`
15914 // presence-probe arm must key off [`Caixa::contratos`], not the
15915 // raw `!self.contratos.is_empty()` field-probe. Structurally: a
15916 // `Caixa { contratos: vec![WitContract { .. }], .. }` must push
15917 // `M3_AUTHOR_KEY_CONTRATOS` onto the declared-slot list (the
15918 // presence bit is non-empty, so the mesh kind-coherence gate
15919 // must surface the slot as "declared"), and a `Caixa {
15920 // contratos: vec![], .. }` must NOT push the label (the "author
15921 // omitted the slot entirely" arm — the empty-slice partition
15922 // the serde-default folds onto). The pair jointly pins the
15923 // accessor + declared-slot enumerator composition: any future
15924 // silent detour that had the accessor collapse
15925 // `[WitContract { .. }]` to `[]` (a `.filter(|c| c.de() !=
15926 // "__reserved__")` projection) would silently absorb the
15927 // "declared but degenerate" arm at the accessor boundary and
15928 // the [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
15929 // coherence gate would silently accept a struct-literal
15930 // `Caixa` carrying the drift.
15931 //
15932 // Peer of the sibling
15933 // `declared_servico_slots_upgrade_from_arm_routes_through_accessor`
15934 // (2a1f907),
15935 // `declared_supervisor_slots_children_arm_routes_through_accessor`
15936 // (c17b51e), and
15937 // `declared_mesh_slots_membros_arm_routes_through_accessor`
15938 // (0f26987) composition pins on the M2 `:upgrade-from` /
15939 // `:children` / M3 `:membros` composite-slice arms — same "the
15940 // enumerator gate must route through the substrate-primitive
15941 // typed dispatch" discipline extended onto the M3 `:contratos`
15942 // composite-slice arm, closing the M3 mesh-slot arm of the
15943 // declared-slot enumerator's routing invariant on the
15944 // composite-slice inputs.
15945 let c = caixa_aplicacao_with_contratos(vec![contrato_http_for_test(
15946 "cart", "catalog", "/items",
15947 )]);
15948 let slots = c.declared_mesh_slots();
15949 assert!(
15950 slots.contains(&crate::render::M3_AUTHOR_KEY_CONTRATOS),
15951 "declared_mesh_slots must push M3_AUTHOR_KEY_CONTRATOS when \
15952 `:contratos` is non-empty — the accessor and the enumerator \
15953 gate must route through the same substrate-primitive \
15954 typed dispatch on the outer :contratos presence bit (got \
15955 slots={slots:?})",
15956 );
15957 let c = caixa_aplicacao_with_contratos(vec![]);
15958 let slots = c.declared_mesh_slots();
15959 assert!(
15960 !slots.contains(&crate::render::M3_AUTHOR_KEY_CONTRATOS),
15961 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_CONTRATOS \
15962 when `:contratos` is empty — the author-omitted arm must \
15963 route through the accessor's empty-slice return unchanged \
15964 (got slots={slots:?})",
15965 );
15966 }
15967
15968 #[test]
15969 fn aplicacao_view_contratos_arm_routes_through_accessor() {
15970 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:contratos`
15971 // fold-in arm must key off [`Caixa::contratos`], not the raw
15972 // `self.contratos.clone()` field-clone. Structurally: a `Caixa
15973 // { kind: Aplicacao, contratos: vec![WitContract { de: "cart",
15974 // .. }, WitContract { de: "pricing", .. }], .. }` must fold the
15975 // per-edge list through the accessor into the typed
15976 // [`crate::AplicacaoSpec`] view's `contratos` slot verbatim —
15977 // every entry the accessor surfaces must land in the view's
15978 // `contratos` slot in the same order. The pair jointly pins
15979 // the accessor + view-composer composition: a future silent
15980 // detour that had the accessor shuffle or drop an edge would
15981 // silently split the paired declared-slot enumerator's
15982 // presence bit from the typed-view composer's edge-list, a
15983 // two-consumer split at the enumerator and the view composer
15984 // far from the source `caixa.lisp`.
15985 //
15986 // Peer of the sibling
15987 // `aplicacao_view_membros_arm_routes_through_accessor`
15988 // (0f26987) composition pin on the M3 `:membros` outer-
15989 // `&[Composite]` composite-slice arm, closing the aplicacao-
15990 // view composer's routing invariant on the composite-slice
15991 // inputs at the outer altitude.
15992 let c = caixa_aplicacao_with_contratos(vec![
15993 contrato_http_for_test("cart", "catalog", "/items"),
15994 contrato_http_for_test("cart", "pricing", "/price"),
15995 ]);
15996 let view = c
15997 .aplicacao_view()
15998 .expect("Aplicacao kind must produce an aplicacao_view");
15999 assert_eq!(
16000 view.contratos(),
16001 c.contratos(),
16002 "aplicacao_view must fold Caixa::contratos verbatim into \
16003 AplicacaoSpec::contratos — the accessor and the view \
16004 composer must route through the same substrate-primitive \
16005 typed dispatch on the outer :contratos slice (got view \
16006 contratos={:?}, expected {:?})",
16007 view.contratos(),
16008 c.contratos(),
16009 );
16010 }
16011
16012 #[test]
16013 fn contratos_projects_slice_by_borrow() {
16014 // The by-borrow pin: [`Caixa::contratos`] returns `&[WitContract]`
16015 // by borrow — the returned slice borrows the underlying
16016 // `Vec<WitContract>` storage of the `:contratos` slot and the
16017 // accessor must not clone the backing `Vec` on every call.
16018 // Peer of the sibling outer top-level [`Caixa`] `&[T]`-return
16019 // by-borrow pins (`autores_projects_slice_by_borrow` b5d813f,
16020 // `etiquetas_projects_slice_by_borrow` 78c7d3c,
16021 // `bibliotecas_projects_slice_by_borrow` 8a36c23,
16022 // `exe_projects_slice_by_borrow` 65d9527,
16023 // `servicos_projects_slice_by_borrow` 611f78b,
16024 // `deps_projects_slice_by_borrow` ad34b4e,
16025 // `deps_dev_projects_slice_by_borrow` f7fd81e,
16026 // `upgrade_from_projects_slice_by_borrow` 2a1f907,
16027 // `children_projects_slice_by_borrow` c17b51e,
16028 // `membros_projects_slice_by_borrow` 0f26987) on the sibling
16029 // outer top-level [`Caixa`] scalar-element and composite-
16030 // element `&[T]` axes — closes the outer-`Caixa` M3 mesh-slot
16031 // composite-element `&[Composite]` axis on the by-borrow pin:
16032 // the accessor's returned slice must borrow from `&self` (the
16033 // returned reference's lifetime is tied to `&self`), and
16034 // calling the accessor twice on the same [`Caixa`] must yield
16035 // slices that are pointer-equal (the underlying byte-buffer is
16036 // the storage `Vec`'s allocation, not a fresh copy) as well as
16037 // value-equal (idempotent, no side effects on `&self`).
16038 //
16039 // Pins against a future silent detour that returned an owned
16040 // `Vec<WitContract>` (which would type-check but silently clone
16041 // on every call), a `&Vec<WitContract>` return (which would
16042 // leak the backing `Vec`'s grow/push/reserve surface no
16043 // downstream consumer reaches for), or a one-arm-only accessor
16044 // that returned a saturating value on some sentinel input.
16045 for contratos in [
16046 vec![],
16047 vec![contrato_http_for_test("cart", "catalog", "/items")],
16048 vec![
16049 contrato_http_for_test("cart", "catalog", "/items"),
16050 contrato_http_for_test("cart", "pricing", "/price"),
16051 ],
16052 ] {
16053 let c = caixa_aplicacao_with_contratos(contratos.clone());
16054 let first = c.contratos();
16055 let second = c.contratos();
16056 assert_eq!(
16057 first, second,
16058 "Caixa::contratos must be idempotent — two successive \
16059 calls on the same &self must return the same \
16060 &[WitContract]",
16061 );
16062 assert_eq!(
16063 first.as_ptr(),
16064 second.as_ptr(),
16065 "Caixa::contratos must borrow the underlying \
16066 Vec<WitContract> storage — two successive calls must \
16067 return slices with the same backing pointer (a fresh \
16068 Vec<WitContract> clone would change the pointer on \
16069 every call)",
16070 );
16071 assert_eq!(
16072 first,
16073 contratos.as_slice(),
16074 "Caixa::contratos must return :contratos verbatim by \
16075 borrow — got {first:?}, expected {contratos:?}",
16076 );
16077 }
16078 }
16079
16080 // ── drift-detection: Caixa top-level multi-word serde-derive-to-const identity ──
16081
16082 #[test]
16083 fn caixa_multi_word_serde_keys_match_lifted_top_level_key_consts() {
16084 // Load-bearing invariant: every multi-word top-level [`Caixa`]
16085 // serde-derived JSON key routes through a lifted `&'static str`
16086 // const. The Rust field names are `snake_case`
16087 // (`deps_dev` / `upgrade_from` / `max_restarts` /
16088 // `restart_window`); [`Caixa`]'s `#[serde(rename_all =
16089 // "camelCase")]` derive attribute maps each to the camelCase
16090 // byte-string the [`Caixa::to_lisp`] round-trip's
16091 // `serde_json::to_value(self)` step lands under before
16092 // `tatara_lisp::domain::json_to_sexp` re-projects the JSON keys
16093 // to the kebab-case `:deps-dev` / `:upgrade-from` /
16094 // `:max-restarts` / `:restart-window` author surface. Serialize
16095 // a fully-populated [`Caixa`] and pin that each canonical
16096 // byte-sequence appears verbatim in the JSON — a future
16097 // accidental `rename_all = "snake_case"` / `"kebab-case"` /
16098 // verbatim-field-name flip at the derive attribute (any of
16099 // which would silently break every [`Caixa::to_lisp`]
16100 // round-trip and the future M4 operator-side manifest ingest's
16101 // `Value::get(<key>)` navigation) surfaces here as a build-time
16102 // test failure at `manifest.rs`, not as an apply-time
16103 // `.get(<stale-canonical-const>)` returning `None` far from the
16104 // derive-attr drift's commit. Same discipline the sibling
16105 // `supervisor_spec_serde_keys_match_lifted_supervisor_key_consts`
16106 // (40cc4e5), `membro_serde_keys_match_lifted_membro_key_consts`
16107 // (ce80ca0), and `upgrade_from_entry_serde_keys_match_lifted_
16108 // m2_upgrade_from_key_consts` (36ffe65) pins established on the
16109 // sibling M2 supervision-tree, M3 [`Membro`] per-entry, and M2
16110 // [`UpgradeFromEntry`] per-entry axes — extended here to the
16111 // enclosing M0 [`Caixa`] top-level axis so the last of the four
16112 // multi-word top-level [`Caixa`] serde-derived JSON keys
16113 // (`depsDev`) joins the substrate's "one canonical byte-string
16114 // per typed serialized-key axis" discipline.
16115 use crate::supervisor::{ChildSpec, RestartPolicy, RestartStrategy};
16116 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
16117 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
16118 c.deps_dev = vec![Dep::simple("tatara-check", "^0.1")];
16119 c.upgrade_from = vec![UpgradeFromEntry {
16120 from: "0.0.1".into(),
16121 instructions: vec![UpgradeInstruction::Restart],
16122 }];
16123 c.estrategia = Some(RestartStrategy::OneForOne);
16124 c.max_restarts = Some(3);
16125 c.restart_window = Some("60s".into());
16126 c.children = vec![ChildSpec {
16127 caixa: "child".into(),
16128 versao: "^0.1".into(),
16129 restart: RestartPolicy::Permanent,
16130 }];
16131 let json = serde_json::to_string(&c).unwrap();
16132 for key in [
16133 crate::render::CAIXA_KEY_DEPS_DEV,
16134 crate::render::M2_KEY_UPGRADE_FROM,
16135 crate::render::SUPERVISOR_KEY_MAX_RESTARTS,
16136 crate::render::SUPERVISOR_KEY_RESTART_WINDOW,
16137 ] {
16138 let quoted = format!("\"{key}\"");
16139 assert!(
16140 json.contains("ed),
16141 "serialized Caixa must carry the lifted top-level \
16142 multi-word byte-sequence {quoted} verbatim in the JSON \
16143 emission (got: {json})",
16144 );
16145 }
16146 }
16147
16148 #[test]
16149 fn caixa_top_level_multi_word_key_consts_are_pairwise_distinct() {
16150 // Cross-axis drift-detection pin: a future collapse of the four
16151 // canonical [`Caixa`] top-level multi-word byte-strings onto the
16152 // same value (e.g. an accidental copy-paste flip of
16153 // [`crate::render::CAIXA_KEY_DEPS_DEV`] to also read
16154 // `"upgradeFrom"`) would silently reroute every downstream
16155 // `Value::get(<key>)` probe on one axis onto the sibling axis's
16156 // top-level entry and pass every propagation-probe test that
16157 // expected only the stale axis's value. Peer of the sibling
16158 // four-way distinct pin on the `SUPERVISOR_KEY_*` tetrad
16159 // (40cc4e5) and the two-way pin on `MEMBRO_KEY_*` (ce80ca0).
16160 let all = [
16161 crate::render::CAIXA_KEY_DEPS_DEV,
16162 crate::render::M2_KEY_UPGRADE_FROM,
16163 crate::render::SUPERVISOR_KEY_MAX_RESTARTS,
16164 crate::render::SUPERVISOR_KEY_RESTART_WINDOW,
16165 ];
16166 for (i, a) in all.iter().enumerate() {
16167 for b in all.iter().skip(i + 1) {
16168 assert_ne!(
16169 a, b,
16170 "Caixa top-level multi-word key consts must be \
16171 pairwise-distinct canonical byte-sequences — got \
16172 `{a}` == `{b}`",
16173 );
16174 }
16175 }
16176 }
16177
16178 #[test]
16179 fn caixa_top_level_multi_word_key_consts_are_lower_camel_case_shape() {
16180 // Shape-pin: every [`Caixa`] top-level multi-word key const must
16181 // be a lowerCamelCase byte-sequence (no `snake_case`
16182 // underscores, no `kebab-case` hyphens, no leading colon, no
16183 // `PascalCase` leading capital, no whitespace / dots) — the
16184 // canonical shape the `#[serde(rename_all = "camelCase")]`
16185 // derive produces on [`Caixa`]. A future flip to a
16186 // non-camelCase attribute at the derive surfaces both here
16187 // (this test fails on the stale-constant shape) and at
16188 // `caixa_multi_word_serde_keys_match_lifted_top_level_key_consts`
16189 // (that test fails on the mismatch between const and derive).
16190 // Peer with `membro_key_consts_are_lower_camel_case_shape`
16191 // (ce80ca0) and `supervisor_key_consts_are_lower_camel_case_shape`
16192 // (40cc4e5) on the sibling per-entry / supervisor-tree axes.
16193 for key in [
16194 crate::render::CAIXA_KEY_DEPS_DEV,
16195 crate::render::M2_KEY_UPGRADE_FROM,
16196 crate::render::SUPERVISOR_KEY_MAX_RESTARTS,
16197 crate::render::SUPERVISOR_KEY_RESTART_WINDOW,
16198 ] {
16199 assert!(
16200 !key.is_empty(),
16201 "Caixa top-level multi-word key const must be non-empty \
16202 (got {key:?})"
16203 );
16204 let first = key.chars().next().unwrap();
16205 assert!(
16206 first.is_ascii_lowercase(),
16207 "Caixa top-level multi-word key const must lead with an \
16208 ASCII-lowercase byte (got {key:?}, leads with {first:?})",
16209 );
16210 assert!(
16211 key.chars().all(|c| c.is_ascii_alphanumeric()),
16212 "Caixa top-level multi-word key const must be \
16213 ASCII-alphanumeric only — no `_` / `-` / `:` / `.` / \
16214 whitespace (got {key:?})",
16215 );
16216 }
16217 }
16218
16219 #[test]
16220 fn caixa_key_deps_dev_pins_canonical_camel_case_byte_string() {
16221 // Scalar-value pin: the byte-string the
16222 // [`crate::render::CAIXA_KEY_DEPS_DEV`] const resolves to,
16223 // asserted verbatim. A future rebrand (`depsDev` → `devDeps`
16224 // matching Cargo's verbatim `dev-dependencies` axis, `depsDev`
16225 // → `depsTest` matching a hypothetical per-test-target
16226 // vocabulary flip) lands as an edit to exactly one const AND
16227 // one derive attribute — the sibling
16228 // `caixa_multi_word_serde_keys_match_lifted_top_level_key_consts`
16229 // pin already ties the const to the derive attribute, so a
16230 // rebrand that touches only one side of the pair fails at
16231 // caixa-core build time. Same "scalar-value pin per const"
16232 // discipline the sibling
16233 // `m2_top_level_author_key_consts_pin_canonical_kebab_case_labels`
16234 // (f49c8b0) and `contrato_key_consts_pin_canonical_camel_case_labels`
16235 // (ca463a4) pins carry on the peer M2 / M3 top-level slot axes.
16236 assert_eq!(crate::render::CAIXA_KEY_DEPS_DEV, "depsDev");
16237 }
16238
16239 #[test]
16240 fn caixa_key_deps_pins_canonical_byte_string() {
16241 // Scalar-value pin: the byte-string the
16242 // [`crate::render::CAIXA_KEY_DEPS`] const resolves to, asserted
16243 // verbatim. Peer of `caixa_key_deps_dev_pins_canonical_camel_case_byte_string`
16244 // on the two-list dep-graph serialized-key axis — the sibling
16245 // pin covers the multi-word `deps_dev → depsDev` camelCase
16246 // arm, this pin covers the single-word `deps → deps` no-op arm
16247 // (the [`crate::Caixa::deps`] field name carries no `_`, so the
16248 // `#[serde(rename_all = "camelCase")]` derive is a no-op on this
16249 // axis and the emitted JSON key equals the source-side field
16250 // name byte-for-byte). A future [`crate::Caixa::deps`] field
16251 // rename (`deps` → `dependencies` matching Cargo's verbatim
16252 // `[dependencies]` axis, `deps` → `runtime_deps` matching a
16253 // hypothetical per-runtime-target vocabulary flip) OR an added
16254 // `#[serde(rename = "…")]` explicit override lands as an edit
16255 // to exactly one const AND one derive-attr / field name — the
16256 // sibling `caixa_deps_serde_key_matches_lifted_caixa_key_deps`
16257 // pin ties the const to the emitted JSON key, so a rebrand
16258 // that touches only one side of the pair fails at caixa-core
16259 // build time.
16260 assert_eq!(crate::render::CAIXA_KEY_DEPS, "deps");
16261 }
16262
16263 #[test]
16264 fn caixa_deps_serde_key_matches_lifted_caixa_key_deps() {
16265 // Load-bearing invariant on the single-word `deps` top-level
16266 // axis: the byte-string [`crate::render::CAIXA_KEY_DEPS`] pins
16267 // must appear verbatim in the JSON [`Caixa::to_lisp`]'s
16268 // `serde_json::to_value(self)` step emits. Serialize a
16269 // populated [`Caixa`] whose `:deps` slot carries at least one
16270 // entry (the `#[serde(default)]` attribute on the field emits
16271 // an empty `[]` even without members, but a non-empty vec
16272 // additionally covers the codec's per-`Dep`-entry emission
16273 // path) and pin that `"deps"` appears verbatim in the JSON
16274 // emission — a future accidental `rename_all = "snake_case"` /
16275 // `"kebab-case"` flip at the derive attribute (or an added
16276 // `#[serde(rename = "…")]` explicit override on the field, or
16277 // a Rust field rename) would break every [`Caixa::to_lisp`]
16278 // round-trip and the future M4 operator-side manifest ingest's
16279 // `Value::get(CAIXA_KEY_DEPS)` navigation — surfaces here as a
16280 // build-time test failure at `manifest.rs`, not as an
16281 // apply-time `.get(<stale-canonical-const>)` returning `None`
16282 // far from the drift's commit. Peer of the sibling
16283 // `caixa_multi_word_serde_keys_match_lifted_top_level_key_consts`
16284 // multi-word pin on the same M0 [`Caixa`] top-level
16285 // serialized-key axis, extended here to the single-word arm
16286 // the multi-word test's `rename_all = "camelCase"` sweep can't
16287 // reach (single-word `deps → deps` is a no-op the multi-word
16288 // pin's `\"depsDev\"` / `\"upgradeFrom\"` / `\"maxRestarts\"` /
16289 // `\"restartWindow\"` byte-scan can never observe).
16290 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
16291 c.deps = vec![Dep::simple("caixa-core", "^0.1")];
16292 let json = serde_json::to_string(&c).unwrap();
16293 let quoted = format!("\"{}\"", crate::render::CAIXA_KEY_DEPS);
16294 assert!(
16295 json.contains("ed),
16296 "serialized Caixa must carry the lifted top-level `deps` \
16297 byte-sequence {quoted} verbatim in the JSON emission (got: \
16298 {json})",
16299 );
16300 }
16301
16302 #[test]
16303 fn caixa_dep_graph_two_list_key_consts_are_pairwise_distinct() {
16304 // Cross-axis drift-detection pin on the two-list dep-graph
16305 // renderer-side wire-key axis: a future collapse of the
16306 // canonical [`crate::render::CAIXA_KEY_DEPS`] /
16307 // [`crate::render::CAIXA_KEY_DEPS_DEV`] byte-strings onto the
16308 // same value (e.g. an accidental copy-paste flip of
16309 // `CAIXA_KEY_DEPS_DEV` to also read `"deps"`) would silently
16310 // reroute every downstream `Value::get(<key>)` probe on one
16311 // axis onto the sibling axis's dep-list and pass every
16312 // propagation-probe test that expected only the stale axis's
16313 // value — a dev-only dep would land in the runtime closure at
16314 // publish time, or a runtime dep would be excluded from the
16315 // published lacre. Peer of the sibling four-way distinct pin
16316 // on the top-level multi-word tetrad
16317 // (`caixa_top_level_multi_word_key_consts_are_pairwise_distinct`)
16318 // and the two-way pin on the sibling
16319 // [`DEP_AUTHOR_KEY_DEPS`] / [`DEP_AUTHOR_KEY_DEPS_DEV`]
16320 // author-facing arm (4da6fba's test), extended here to the
16321 // renderer-side wire-key arm of the same two-list dep-graph
16322 // axis so both halves of the "one canonical byte-string per
16323 // typed axis per (author, wire)" grid carry the same
16324 // distinct-ness discipline.
16325 assert_ne!(
16326 crate::render::CAIXA_KEY_DEPS,
16327 crate::render::CAIXA_KEY_DEPS_DEV,
16328 "CAIXA_KEY_DEPS and CAIXA_KEY_DEPS_DEV must be distinct \
16329 canonical byte-sequences on the two-list dep-graph \
16330 renderer-side wire-key axis"
16331 );
16332 }
16333}